diff --git a/.eslintignore b/.eslintignore index 972c818d791bf..93c69b4f9b207 100644 --- a/.eslintignore +++ b/.eslintignore @@ -39,7 +39,7 @@ target /x-pack/legacy/plugins/maps/public/vendor/** # package overrides -/packages/eslint-config-kibana +/packages/elastic-eslint-config-kibana /packages/kbn-interpreter/src/common/lib/grammar.js /packages/kbn-plugin-generator/template /packages/kbn-pm/dist diff --git a/.eslintrc.js b/.eslintrc.js index ff4ac180c3774..3161a25b70870 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -94,12 +94,6 @@ module.exports = { 'jsx-a11y/no-onchange': 'off', }, }, - { - files: ['src/plugins/es_ui_shared/**/*.{js,mjs,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: ['src/plugins/kibana_react/**/*.{js,mjs,ts,tsx}'], rules: { @@ -125,25 +119,12 @@ module.exports = { 'jsx-a11y/click-events-have-key-events': 'off', }, }, - { - files: ['x-pack/legacy/plugins/index_management/**/*.{js,mjs,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - 'react-hooks/rules-of-hooks': 'off', - }, - }, { files: ['x-pack/plugins/ml/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, - { - files: ['x-pack/legacy/plugins/snapshot_restore/**/*.{js,mjs,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, /** * Files that require Apache 2.0 headers, settings @@ -324,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 66fb31cc91d5a..bcb4774475849 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -63,6 +63,13 @@ /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @watson @vigneshshanmugam +# Client Side Monitoring (lives in APM directories but owned by Uptime) +/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum @elastic/uptime +/x-pack/plugins/apm/public/application/csmApp.tsx @elastic/uptime +/x-pack/plugins/apm/public/components/app/RumDashboard @elastic/uptime +/x-pack/plugins/apm/server/lib/rum_client @elastic/uptime +/x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime + # Beats /x-pack/legacy/plugins/beats_management/ @elastic/beats @@ -85,7 +92,6 @@ /x-pack/plugins/ingest_manager/ @elastic/ingest-management /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/observability-ui -/x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime @@ -210,8 +216,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/.github/ISSUE_TEMPLATE/APM.md b/.github/ISSUE_TEMPLATE/APM.md new file mode 100644 index 0000000000000..983806f70bc3f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/APM.md @@ -0,0 +1,11 @@ +--- +name: APM Issue +about: Issues related to the APM solution in Kibana +labels: Team:apm +title: [APM] +--- + +**Versions** +Kibana: (if relevant) +APM Server: (if relevant) +Elasticsearch: (if relevant) 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/canvas/canvas-tinymath-functions.asciidoc b/docs/canvas/canvas-tinymath-functions.asciidoc index 73808fc6625d1..f92f7c642a2ee 100644 --- a/docs/canvas/canvas-tinymath-functions.asciidoc +++ b/docs/canvas/canvas-tinymath-functions.asciidoc @@ -492,37 +492,6 @@ find the mean by index. |one or more numbers or arrays of numbers |=== -*Returns*: `number` | `Array.`. The maximum value of all numbers if -`args` contains only numbers. Returns an array with the the maximum values at each -index, including all scalar numbers in `args` in the calculation at each index if -`args` contains at least one array. - -*Throws*: `'Array length mismatch'` if `args` contains arrays of different lengths - -*Example* -[source, js] ------------- -max(1, 2, 3) // returns 3 -max([10, 20, 30, 40], 15) // returns [15, 20, 30, 40] -max([1, 9], 4, [3, 5]) // returns [max([1, 4, 3]), max([9, 4, 5])] = [4, 9] ------------- - -[float] -=== mean( ...args ) - -Finds the mean value of one of more numbers/arrays of numbers passed into the function. -If at least one array of numbers is passed into the function, the function will -find the mean by index. - -[cols="3*^<"] -|=== -|Param |Type |Description - -|...args -|number \| Array. -|one or more numbers or arrays of numbers -|=== - *Returns*: `number` | `Array.`. The mean value of all numbers if `args` contains only numbers. Returns an array with the the mean values of each index, including all scalar numbers in `args` in the calculation at each index if `args` 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/developer/contributing/development-github.asciidoc b/docs/developer/contributing/development-github.asciidoc index a6d4e29940487..84f51843098a7 100644 --- a/docs/developer/contributing/development-github.asciidoc +++ b/docs/developer/contributing/development-github.asciidoc @@ -1,5 +1,5 @@ [[development-github]] -== How we use git and github +== How we use Git and GitHub [discrete] === Forking @@ -12,17 +12,21 @@ repo, which we'll refer to in later code snippets. [discrete] === Branching -* All work on the next major release goes into master. -* Past major release branches are named `{majorVersion}.x`. They contain -work that will go into the next minor release. For example, if the next -minor release is `5.2.0`, work for it should go into the `5.x` branch. -* Past minor release branches are named `{majorVersion}.{minorVersion}`. -They contain work that will go into the next patch release. For example, -if the next patch release is `5.3.1`, work for it should go into the -`5.3` branch. -* All work is done on feature branches and merged into one of these -branches. -* Where appropriate, we'll backport changes into older release branches. +At Elastic, all products in the stack, including Kibana, are released at the same time with the same version number. Most of these projects have the following branching strategy: + +* `master` is the next major version. +* `.x` is the next minor version. +* `.` is the next release of a minor version, including patch releases. + +As an example, let's assume that the `7.x` branch is currently a not-yet-released `7.6.0`. Once `7.6.0` has reached feature freeze, it will be branched to `7.6` and `7.x` will be updated to reflect `7.7.0`. The release of `7.6.0` and subsequent patch releases will be cut from the `7.6` branch. At any time, you can verify the current version of a branch by inspecting the `version` attribute in the `package.json` file within the Kibana source. + +Pull requests are made into the `master` branch and then backported when it is safe and appropriate. + +* Breaking changes do not get backported and only go into `master`. +* All non-breaking changes can be backported to the `.x` branch. +* Features should not be backported to a `.` branch. +* Bugs can be backported to a `.` branch if the changes are safe and appropriate. Safety is a judgment call you make based on factors like the bug's severity, test coverage, confidence in the changes, etc. Your reasoning should be included in the pull request description. +* Documentation changes can be backported to any branch at any time. [discrete] === Commits and Merging @@ -109,4 +113,4 @@ Assuming you've successfully rebased and you're happy with the code, you should [discrete] === Creating a pull request -See <> for the next steps on getting your code changes merged into {kib}. \ No newline at end of file +See <> for the next steps on getting your code changes merged into {kib}. diff --git a/docs/developer/getting-started/running-kibana-advanced.asciidoc b/docs/developer/getting-started/running-kibana-advanced.asciidoc index 44897184f88f2..277e52a3dc8e9 100644 --- a/docs/developer/getting-started/running-kibana-advanced.asciidoc +++ b/docs/developer/getting-started/running-kibana-advanced.asciidoc @@ -48,7 +48,7 @@ If you’re installing dependencies and seeing an error that looks something like .... -Unsupported URL Type: link:packages/eslint-config-kibana +Unsupported URL Type: link:packages/elastic-eslint-config-kibana .... you’re likely running `npm`. To install dependencies in {kib} you diff --git a/docs/development/core/public/kibana-plugin-core-public.app.capabilities.md b/docs/development/core/public/kibana-plugin-core-public.app.capabilities.md new file mode 100644 index 0000000000000..4a027a6ab132c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.capabilities.md @@ -0,0 +1,13 @@ + + +[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) + +## App.capabilities property + +Custom capabilities defined by the app. + +Signature: + +```typescript +capabilities?: Partial; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.category.md b/docs/development/core/public/kibana-plugin-core-public.app.category.md new file mode 100644 index 0000000000000..a1e74f2bcf5e2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.category.md @@ -0,0 +1,13 @@ + + +[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) + +## App.category property + +The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference + +Signature: + +```typescript +category?: AppCategory; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.defaultpath.md b/docs/development/core/public/kibana-plugin-core-public.app.defaultpath.md new file mode 100644 index 0000000000000..3c952ec053e62 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.defaultpath.md @@ -0,0 +1,13 @@ + + +[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) + +## 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. + +Signature: + +```typescript +defaultPath?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.euiicontype.md b/docs/development/core/public/kibana-plugin-core-public.app.euiicontype.md new file mode 100644 index 0000000000000..ff79d832f92e2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.euiicontype.md @@ -0,0 +1,13 @@ + + +[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) + +## App.euiIconType property + +A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. + +Signature: + +```typescript +euiIconType?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.icon.md b/docs/development/core/public/kibana-plugin-core-public.app.icon.md new file mode 100644 index 0000000000000..98260da5d2021 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.icon.md @@ -0,0 +1,13 @@ + + +[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) + +## App.icon property + +A URL to an image file used as an icon. Used as a fallback if `euiIconType` is not provided. + +Signature: + +```typescript +icon?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.id.md b/docs/development/core/public/kibana-plugin-core-public.app.id.md new file mode 100644 index 0000000000000..9899cfc0cf572 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.id.md @@ -0,0 +1,13 @@ + + +[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) + +## App.id property + +The unique identifier of the application + +Signature: + +```typescript +id: string; +``` 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.app.navlinkstatus.md b/docs/development/core/public/kibana-plugin-core-public.app.navlinkstatus.md new file mode 100644 index 0000000000000..c01a26e42e237 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.navlinkstatus.md @@ -0,0 +1,13 @@ + + +[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) + +## 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) + +Signature: + +```typescript +navLinkStatus?: AppNavLinkStatus; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.order.md b/docs/development/core/public/kibana-plugin-core-public.app.order.md new file mode 100644 index 0000000000000..bb6be116b6b58 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.order.md @@ -0,0 +1,13 @@ + + +[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) + +## App.order property + +An ordinal used to sort nav links relative to one another for display. + +Signature: + +```typescript +order?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.status.md b/docs/development/core/public/kibana-plugin-core-public.app.status.md new file mode 100644 index 0000000000000..caa6ff1dcac9e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.status.md @@ -0,0 +1,13 @@ + + +[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) + +## App.status property + +The initial status of the application. Defaulting to `accessible` + +Signature: + +```typescript +status?: AppStatus; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.title.md b/docs/development/core/public/kibana-plugin-core-public.app.title.md new file mode 100644 index 0000000000000..c705e3ab8d2b1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.title.md @@ -0,0 +1,13 @@ + + +[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) + +## App.title property + +The title of the application. + +Signature: + +```typescript +title: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.tooltip.md b/docs/development/core/public/kibana-plugin-core-public.app.tooltip.md new file mode 100644 index 0000000000000..e901de0fdccc9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.tooltip.md @@ -0,0 +1,13 @@ + + +[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) + +## App.tooltip property + +A tooltip shown when hovering over app link. + +Signature: + +```typescript +tooltip?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.updater_.md b/docs/development/core/public/kibana-plugin-core-public.app.updater_.md new file mode 100644 index 0000000000000..67acccbd02965 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.updater_.md @@ -0,0 +1,44 @@ + + +[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) + +## 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. + +Signature: + +```typescript +updater$?: Observable; +``` + +## Example + +How to update an application navLink at runtime + +```ts +// inside your plugin's setup function +export class MyPlugin implements Plugin { + private appUpdater = new BehaviorSubject(() => ({})); + + setup({ application }) { + application.register({ + id: 'my-app', + title: 'My App', + updater$: this.appUpdater, + async mount(params) { + const { renderApp } = await import('./application'); + return renderApp(params); + }, + }); + } + + start() { + // later, when the navlink needs to be updated + appUpdater.next(() => { + navLinkStatus: AppNavLinkStatus.disabled, + }) + } + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.capabilities.md b/docs/development/core/public/kibana-plugin-core-public.appbase.capabilities.md deleted file mode 100644 index 3dd440c4253b3..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.capabilities.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) > [capabilities](./kibana-plugin-core-public.appbase.capabilities.md) - -## AppBase.capabilities property - -Custom capabilities defined by the app. - -Signature: - -```typescript -capabilities?: Partial; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.category.md b/docs/development/core/public/kibana-plugin-core-public.appbase.category.md deleted file mode 100644 index 29532a15747e1..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.category.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) > [category](./kibana-plugin-core-public.appbase.category.md) - -## AppBase.category property - -The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference - -Signature: - -```typescript -category?: AppCategory; -``` 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.defaultpath.md b/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md deleted file mode 100644 index 51492756ef232..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.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) > [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) - -## AppBase.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. - -Signature: - -```typescript -defaultPath?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.euiicontype.md b/docs/development/core/public/kibana-plugin-core-public.appbase.euiicontype.md deleted file mode 100644 index e5bfa38097361..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.euiicontype.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) > [euiIconType](./kibana-plugin-core-public.appbase.euiicontype.md) - -## AppBase.euiIconType property - -A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. - -Signature: - -```typescript -euiIconType?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.icon.md b/docs/development/core/public/kibana-plugin-core-public.appbase.icon.md deleted file mode 100644 index 0bd67922dc39c..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.icon.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) > [icon](./kibana-plugin-core-public.appbase.icon.md) - -## AppBase.icon property - -A URL to an image file used as an icon. Used as a fallback if `euiIconType` is not provided. - -Signature: - -```typescript -icon?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.id.md b/docs/development/core/public/kibana-plugin-core-public.appbase.id.md deleted file mode 100644 index 6c0ec462fa16b..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.id.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) > [id](./kibana-plugin-core-public.appbase.id.md) - -## AppBase.id property - -The unique identifier of the application - -Signature: - -```typescript -id: string; -``` 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.appbase.navlinkstatus.md b/docs/development/core/public/kibana-plugin-core-public.appbase.navlinkstatus.md deleted file mode 100644 index decfb235b2858..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.navlinkstatus.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) > [navLinkStatus](./kibana-plugin-core-public.appbase.navlinkstatus.md) - -## AppBase.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) - -Signature: - -```typescript -navLinkStatus?: AppNavLinkStatus; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.order.md b/docs/development/core/public/kibana-plugin-core-public.appbase.order.md deleted file mode 100644 index 606a40e72d592..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.order.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) > [order](./kibana-plugin-core-public.appbase.order.md) - -## AppBase.order property - -An ordinal used to sort nav links relative to one another for display. - -Signature: - -```typescript -order?: number; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.status.md b/docs/development/core/public/kibana-plugin-core-public.appbase.status.md deleted file mode 100644 index 4d6ba6ebd955e..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.status.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) > [status](./kibana-plugin-core-public.appbase.status.md) - -## AppBase.status property - -The initial status of the application. Defaulting to `accessible` - -Signature: - -```typescript -status?: AppStatus; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.title.md b/docs/development/core/public/kibana-plugin-core-public.appbase.title.md deleted file mode 100644 index d6058badee8e8..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.title.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) > [title](./kibana-plugin-core-public.appbase.title.md) - -## AppBase.title property - -The title of the application. - -Signature: - -```typescript -title: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.tooltip.md b/docs/development/core/public/kibana-plugin-core-public.appbase.tooltip.md deleted file mode 100644 index 0c0b0840eb921..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.tooltip.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) > [tooltip](./kibana-plugin-core-public.appbase.tooltip.md) - -## AppBase.tooltip property - -A tooltip shown when hovering over app link. - -Signature: - -```typescript -tooltip?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.updater_.md b/docs/development/core/public/kibana-plugin-core-public.appbase.updater_.md deleted file mode 100644 index c2c572755f9b2..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.updater_.md +++ /dev/null @@ -1,44 +0,0 @@ - - -[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) - -## AppBase.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. - -Signature: - -```typescript -updater$?: Observable; -``` - -## Example - -How to update an application navLink at runtime - -```ts -// inside your plugin's setup function -export class MyPlugin implements Plugin { - private appUpdater = new BehaviorSubject(() => ({})); - - setup({ application }) { - application.register({ - id: 'my-app', - title: 'My App', - updater$: this.appUpdater, - async mount(params) { - const { renderApp } = await import('./application'); - return renderApp(params); - }, - }); - } - - start() { - // later, when the navlink needs to be updated - appUpdater.next(() => { - navLinkStatus: AppNavLinkStatus.disabled, - }) - } - -``` - 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.appmountparameters.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md index de79fc8281c45..f6c57603bedde 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.md @@ -19,4 +19,5 @@ export interface AppMountParameters | [element](./kibana-plugin-core-public.appmountparameters.element.md) | HTMLElement | The container element to render the application into. | | [history](./kibana-plugin-core-public.appmountparameters.history.md) | ScopedHistory<HistoryLocationState> | A scoped history instance for your application. Should be used to wire up your applications Router. | | [onAppLeave](./kibana-plugin-core-public.appmountparameters.onappleave.md) | (handler: AppLeaveHandler) => void | A function that can be used to register a handler that will be called when the user is leaving the current application, allowing to prompt a confirmation message before actually changing the page.This will be called either when the user goes to another application, or when trying to close the tab or manually changing the url. | +| [setHeaderActionMenu](./kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md) | (menuMount: MountPoint | undefined) => void | A function that can be used to set the mount point used to populate the application action container in the chrome header.Calling the handler multiple time will erase the current content of the action menu with the mount from the latest call. Calling the handler with undefined will unmount the current mount point. Calling the handler after the application has been unmounted will have no effect. | diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md new file mode 100644 index 0000000000000..ca9cee64bb1f9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) > [setHeaderActionMenu](./kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md) + +## AppMountParameters.setHeaderActionMenu property + +A function that can be used to set the mount point used to populate the application action container in the chrome header. + +Calling the handler multiple time will erase the current content of the action menu with the mount from the latest call. Calling the handler with `undefined` will unmount the current mount point. Calling the handler after the application has been unmounted will have no effect. + +Signature: + +```typescript +setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; +``` + +## Example + + +```ts +// application.tsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter, Route } from 'react-router-dom'; + +import { CoreStart, AppMountParameters } from 'src/core/public'; +import { MyPluginDepsStart } from './plugin'; + +export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => { + const { renderApp } = await import('./application'); + const { renderActionMenu } = await import('./action_menu'); + setHeaderActionMenu((element) => { + return renderActionMenu(element); + }) + return renderApp({ element, history }); +} + +``` + 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/public/kibana-plugin-core-public.savedobjectsfindoptions.filter.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.filter.md index 900f8e333f337..2c20fe2dab00f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.filter.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.filter.md @@ -7,5 +7,5 @@ Signature: ```typescript -filter?: string; +filter?: string | KueryNode; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index ebd0a99531755..903462ac3039d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -17,7 +17,7 @@ export interface SavedObjectsFindOptions | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-core-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-core-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | -| [filter](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | string | | +| [filter](./kibana-plugin-core-public.savedobjectsfindoptions.filter.md) | string | KueryNode | | | [hasReference](./kibana-plugin-core-public.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [namespaces](./kibana-plugin-core-public.savedobjectsfindoptions.namespaces.md) | string[] | | | [page](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.filter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.filter.md index ae7b7a28bcd09..c98a4fe5e8796 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.filter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.filter.md @@ -7,5 +7,5 @@ Signature: ```typescript -filter?: string; +filter?: string | KueryNode; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 15a9d99b3d062..804c83f7c1b48 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -17,7 +17,7 @@ export interface SavedObjectsFindOptions | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-core-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-core-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | -| [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string | | +| [filter](./kibana-plugin-core-server.savedobjectsfindoptions.filter.md) | string | KueryNode | | | [hasReference](./kibana-plugin-core-server.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [namespaces](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md) | string[] | | | [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | number | | 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.init.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md index ce401bec87dbb..595992dc82b74 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md @@ -7,15 +7,8 @@ Signature: ```typescript -init(forceFieldRefresh?: boolean): Promise; +init(): Promise; ``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| forceFieldRefresh | boolean | | - Returns: `Promise` 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 c15cb3358f689..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 @@ -50,7 +50,7 @@ export declare class IndexPattern implements IIndexPattern | [getScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md) | | | | [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | | | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | -| [init(forceFieldRefresh)](./kibana-plugin-plugins-data-public.indexpattern.init.md) | | | +| [init()](./kibana-plugin-plugins-data-public.indexpattern.init.md) | | | | [initFromSpec(spec)](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeBasedWildcard()](./kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.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/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index e139b326b7500..9f3ed8c1263ba 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` 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.abortsignal.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md new file mode 100644 index 0000000000000..693345f480a9a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md @@ -0,0 +1,13 @@ + + +[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.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/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.signal.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.signal.md deleted file mode 100644 index 948dfd66da7a0..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.signal.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[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) - -## ISearchOptions.signal property - -An `AbortSignal` that allows the caller of `search` to abort a search request. - -Signature: - -```typescript -signal?: AbortSignal; -``` 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/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 9f13c152b4cbe..a64a0330ae43f 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -150,6 +150,12 @@ working on big documents. ==== Machine learning [horizontal] +`ml:anomalyDetection:results:enableTimeDefaults`:: Use the default time filter +in the *Single Metric Viewer* and *Anomaly Explorer*. If this setting is +disabled, the results for the full time range are shown. +`ml:anomalyDetection:results:timeDefaults`:: Sets the default time filter for +viewing {anomaly-job} results. This setting must contain `from` and `to` values (see {ref}/common-options.html#date-math[accepted formats]). It is ignored +unless `ml:anomalyDetection:results:enableTimeDefaults` is enabled. `ml:fileDataVisualizerMaxFileSize`:: Sets the file size limit when importing data in the {data-viz}. The default value is `100MB`. The highest supported value for this setting is `1GB`. 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..13c1d20552fa1 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` - | 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. + +| `xpack.actions.allowedHosts` {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 added to the allowed hosts. 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. + Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically added to allowed hosts. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are added to the allowed hosts 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/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index f6a02b9038c02..83e7edc5a016a 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -12,7 +12,7 @@ Email connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. -Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. +Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is added to the allowed hosts. Port:: The port to connect to on the service provider. Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. Username:: username for 'login' type authentication. diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index 5fd85a1045265..2c9add5233c91 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -132,7 +132,7 @@ This is an irreversible action and impacts all alerts that use this connector. PagerDuty connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is whitelisted. +API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is added to the allowed hosts. Integration Key:: A 32 character PagerDuty Integration Key for an integration on a service, also referred to as the routing key. [float] diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc index 99bf73c0f5597..a1fe7a2521b22 100644 --- a/docs/user/alerting/action-types/slack.asciidoc +++ b/docs/user/alerting/action-types/slack.asciidoc @@ -11,7 +11,7 @@ The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incomin Slack connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is whitelisted. +Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is added to the allowed hosts. [float] [[Preconfigured-slack-configuration]] diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc index c91c24430e982..659c3afad6bd1 100644 --- a/docs/user/alerting/action-types/webhook.asciidoc +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -11,7 +11,7 @@ The Webhook action type uses https://github.com/axios/axios[axios] to send a POS Webhook connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. +URL:: The request URL. If you are using the <> setting, make sure the hostname is added to the allowed hosts. Method:: HTTP request method, either `post`(default) or `put`. Headers:: A set of key-value pairs sent as headers with the request User:: An optional username. If set, HTTP basic authentication is used. Currently only basic authentication is supported. 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/user/dashboard/tutorials.asciidoc b/docs/user/dashboard/tutorials.asciidoc new file mode 100644 index 0000000000000..931720ccbe257 --- /dev/null +++ b/docs/user/dashboard/tutorials.asciidoc @@ -0,0 +1,1756 @@ +[[tutorials]] +== Tutorials + +Learn how to use *Lens*, *Vega*, and *Timelion* by going through one of the step-by-step tutorials. + +[[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] +[[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 +for a more comprehensive +https://vega.github.io/vega-lite/tutorials/getting_started.html[introduction to Vega-Lite], +while only covering the basics. + +In this tutorial, you will build a stacked area chart from one of the {kib} sample data +sets. + +[role="screenshot"] +image::visualize/images/vega_lite_tutorial_1.png[] + +Before beginning this tutorial, install the <> +set. + +When you first open the Vega editor in {kib}, you will see a pre-populated +line chart which shows the total number of documents across all your indices +within the time range. + +[role="screenshot"] +image::visualize/images/vega_lite_default.png[] + +The text editor contains a Vega-Lite spec written in https://hjson.github.io/[HJSON], +which is similar to JSON but optimized for human editing. HJSON supports: + +* Comments using // or /* syntax +* Object keys without quotes +* String values without quotes +* Optional commas +* Double or single quotes +* Multiline strings + +[float] +[[small-steps]] +==== Small steps + +Always work on Vega in the smallest steps possible, and save your work frequently. +Small changes will cause unexpected results. Click the "Save" button now. + +The first step is to change the index to one of the <> +sets. Change + +```yaml +index: _all +``` + +to: + +```yaml +index: kibana_sample_data_ecommerce +``` + +Click "Update". The result is probably not what you expect. You should see a flat +line with 0 results. + +You've only changed the index, so the difference must be the query is returning +no results. You can try the <>, +but intuition may be faster for this particular problem. + +In this case, the problem is that you are querying the field `@timestamp`, +which does not exist in the `kibana_sample_data_ecommerce` data. Find and replace +`@timestamp` with `order_date`. This fixes the problem, leaving you with this spec: + +.Expand Vega-Lite spec +[%collapsible%closed] +==== +[source,yaml] +---- +{ + $schema: https://vega.github.io/schema/vega-lite/v4.json + title: Event counts from ecommerce + data: { + url: { + %context%: true + %timefield%: order_date + index: kibana_sample_data_ecommerce + body: { + aggs: { + time_buckets: { + date_histogram: { + field: order_date + interval: {%autointerval%: true} + extended_bounds: { + min: {%timefilter%: "min"} + max: {%timefilter%: "max"} + } + min_doc_count: 0 + } + } + } + size: 0 + } + } + format: {property: "aggregations.time_buckets.buckets" } + } + + mark: line + + encoding: { + x: { + field: key + type: temporal + axis: { title: null } + } + y: { + field: doc_count + type: quantitative + axis: { title: "Document count" } + } + } +} +---- + +==== + +Now, let's make the visualization more interesting by adding another aggregation +to create a stacked area chart. To verify that you have constructed the right +query, it is easiest to use the {kib} Dev Tools in a separate tab from the +Vega editor. Open the Dev Tools from the Management section of the navigation. + +This query is roughly equivalent to the one that is used in the default +Vega-Lite spec. Copy it into the Dev Tools: + +```js +POST kibana_sample_data_ecommerce/_search +{ + "query": { + "range": { + "order_date": { + "gte": "now-7d" + } + } + }, + "aggs": { + "time_buckets": { + "date_histogram": { + "field": "order_date", + "fixed_interval": "1d", + "extended_bounds": { + "min": "now-7d" + }, + "min_doc_count": 0 + } + } + }, + "size": 0 +} +``` + +There's not enough data to create a stacked bar in the original query, so we +will add a new +{ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation]: + +```js +POST kibana_sample_data_ecommerce/_search +{ + "query": { + "range": { + "order_date": { + "gte": "now-7d" + } + } + }, + "aggs": { + "categories": { + "terms": { "field": "category.keyword" }, + "aggs": { + "time_buckets": { + "date_histogram": { + "field": "order_date", + "fixed_interval": "1d", + "extended_bounds": { + "min": "now-7d" + }, + "min_doc_count": 0 + } + } + } + } + }, + "size": 0 +} +``` + +You'll see that the response format looks different from the previous query: + +```json +{ + "aggregations" : { + "categories" : { + "doc_count_error_upper_bound" : 0, + "sum_other_doc_count" : 0, + "buckets" : [{ + "key" : "Men's Clothing", + "doc_count" : 1661, + "time_buckets" : { + "buckets" : [{ + "key_as_string" : "2020-06-30T00:00:00.000Z", + "key" : 1593475200000, + "doc_count" : 19 + }, { + "key_as_string" : "2020-07-01T00:00:00.000Z", + "key" : 1593561600000, + "doc_count" : 71 + }] + } + }] + } + } +} +``` + +Now that we have data that we're happy with, it's time to convert from an +isolated {es} query into a query with {kib} integration. Looking at the +<>, you will +see the full list of special tokens that are used in this query, such +as `%context: true`. This query has also replaced `"fixed_interval": "1d"` +with `interval: {%autointerval%: true}`. Copy the final query into +your spec: + +```yaml + data: { + url: { + %context%: true + %timefield%: order_date + index: kibana_sample_data_ecommerce + body: { + aggs: { + categories: { + terms: { field: "category.keyword" } + aggs: { + time_buckets: { + date_histogram: { + field: order_date + interval: {%autointerval%: true} + extended_bounds: { + min: {%timefilter%: "min"} + max: {%timefilter%: "max"} + } + min_doc_count: 0 + } + } + } + } + } + size: 0 + } + } + format: {property: "aggregations.categories.buckets" } + } +``` + +If you copy and paste that into your Vega-Lite spec, and click "Update", +you will see a warning saying `Infinite extent for field "key": [Infinity, -Infinity]`. +Let's use our <> to understand why. + +Vega-Lite generates data using the names `source_0` and `data_0`. `source_0` contains +the results from the {es} query, and `data_0` contains the visually encoded results +which are shown in the chart. To debug this problem, you need to compare both. + +To look at the source, open the browser dev tools console and type +`VEGA_DEBUG.view.data('source_0')`. You will see: + +```js +[{ + doc_count: 454 + key: "Men's Clothing" + time_buckets: {buckets: Array(57)} + Symbol(vega_id): 12822 +}, ...] +``` + +To compare to the visually encoded data, open the browser dev tools console and type +`VEGA_DEBUG.view.data('data_0')`. You will see: + +```js +[{ + doc_count: 454 + key: NaN + time_buckets: {buckets: Array(57)} + Symbol(vega_id): 13879 +}] +``` + +The issue seems to be that the `key` property is not being converted the right way, +which makes sense because the `key` is now `Men's Clothing` instead of a timestamp. + +To fix this, try updating the `encoding` of your Vega-Lite spec to: + +```yaml + encoding: { + x: { + field: time_buckets.buckets.key + type: temporal + axis: { title: null } + } + y: { + field: time_buckets.buckets.doc_count + type: quantitative + axis: { title: "Document count" } + } + } +``` + +This will show more errors, and you can inspect `VEGA_DEBUG.view.data('data_0')` to +understand why. This now shows: + +```js +[{ + doc_count: 454 + key: "Men's Clothing" + time_buckets: {buckets: Array(57)} + time_buckets.buckets.doc_count: undefined + time_buckets.buckets.key: null + Symbol(vega_id): 14094 +}] +``` + +It looks like the problem is that the `time_buckets` inner array is not being +extracted by Vega. The solution is to use a Vega-lite +https://vega.github.io/vega-lite/docs/flatten.html[flatten transformation], available in {kib} 7.9 and later. +If using an older version of Kibana, the flatten transformation is available in Vega +but not Vega-Lite. + +Add this section in between the `data` and `encoding` section: + +```yaml + transform: [{ + flatten: ["time_buckets.buckets"] + }] +``` + +This does not yet produce the results you expect. Inspect the transformed data +by typing `VEGA_DEBUG.view.data('data_0')` into the console again: + +```js +[{ + doc_count: 453 + key: "Men's Clothing" + time_bucket.buckets.doc_count: undefined + time_buckets: {buckets: Array(57)} + time_buckets.buckets: { + key_as_string: "2020-06-30T15:00:00.000Z", + key: 1593529200000, + doc_count: 2 + } + time_buckets.buckets.key: null + Symbol(vega_id): 21564 +}] +``` + +The debug view shows `undefined` values where you would expect to see numbers, and +the cause is that there are duplicate names which are confusing Vega-Lite. This can +be fixed by making this change to the `transform` and `encoding` blocks: + +```yaml + transform: [{ + flatten: ["time_buckets.buckets"], + as: ["buckets"] + }] + + mark: area + + encoding: { + x: { + field: buckets.key + type: temporal + axis: { title: null } + } + y: { + field: buckets.doc_count + type: quantitative + axis: { title: "Document count" } + } + color: { + field: key + type: nominal + } + } +``` + +At this point, you have a stacked area chart that shows the top categories, +but the chart is still missing some common features that we expect from a {kib} +visualization. Let's add hover states and tooltips next. + +Hover states are handled differently in Vega-Lite and Vega. In Vega-Lite this is +done using a concept called `selection`, which has many permutations that are not +covered in this tutorial. We will be adding a simple tooltip and hover state. + +Because {kib} has enabled the https://vega.github.io/vega-lite/docs/tooltip.html[Vega tooltip plugin], +tooltips can be defined in several ways: + +* Automatic tooltip based on the data, via `{ content: "data" }` +* Array of fields, like `[{ field: "key", type: "nominal" }]` +* Defining a custom Javascript object using the `calculate` transform + +For the simple tooltip, add this to your encoding: + +```yaml + encoding: { + tooltip: [{ + field: buckets.key + type: temporal + title: "Date" + }, { + field: key + type: nominal + title: "Category" + }, { + field: buckets.doc_count + type: quantitative + title: "Count" + }] + } +``` + +As you hover over the area series in your chart, a multi-line tooltip will +appear, but it won't indicate the nearest point that it's pointing to. To +indicate the nearest point, we need to add a second layer. + +The first step is to remove the `mark: area` from your visualization. +Once you've removed the previous mark, add a composite mark at the end of +the Vega-Lite spec: + +```yaml + layer: [{ + mark: area + }, { + mark: point + }] +``` + +You'll see that the points are not appearing to line up with the area chart, +and the reason is that the points are not being stacked. Change your Y encoding +to this: + +```yaml + y: { + field: buckets.doc_count + type: quantitative + axis: { title: "Document count" } + stack: true + } +``` + +Now, we will add a `selection` block inside the point mark: + +```yaml + layer: [{ + mark: area + }, { + mark: point + + selection: { + pointhover: { + type: single + on: mouseover + clear: mouseout + empty: none + fields: ["buckets.key", "key"] + nearest: true + } + } + + encoding: { + size: { + condition: { + selection: pointhover + value: 100 + } + value: 5 + } + fill: { + condition: { + selection: pointhover + value: white + } + } + } + }] +``` + +Now that you've enabled a selection, try moving the mouse around the visualization +and seeing the points respond to the nearest position: + +[role="screenshot"] +image::visualize/images/vega_lite_tutorial_2.png[] + +The final result of this tutorial is this spec: + +.Expand final Vega-Lite spec +[%collapsible%closed] +==== +[source,yaml] +---- +{ + $schema: https://vega.github.io/schema/vega-lite/v4.json + title: Event counts from ecommerce + data: { + url: { + %context%: true + %timefield%: order_date + index: kibana_sample_data_ecommerce + body: { + aggs: { + categories: { + terms: { field: "category.keyword" } + aggs: { + time_buckets: { + date_histogram: { + field: order_date + interval: {%autointerval%: true} + extended_bounds: { + min: {%timefilter%: "min"} + max: {%timefilter%: "max"} + } + min_doc_count: 0 + } + } + } + } + } + size: 0 + } + } + format: {property: "aggregations.categories.buckets" } + } + + transform: [{ + flatten: ["time_buckets.buckets"] + as: ["buckets"] + }] + + encoding: { + x: { + field: buckets.key + type: temporal + axis: { title: null } + } + y: { + field: buckets.doc_count + type: quantitative + axis: { title: "Document count" } + stack: true + } + color: { + field: key + type: nominal + title: "Category" + } + tooltip: [{ + field: buckets.key + type: temporal + title: "Date" + }, { + field: key + type: nominal + title: "Category" + }, { + field: buckets.doc_count + type: quantitative + title: "Count" + }] + } + + layer: [{ + mark: area + }, { + mark: point + + selection: { + pointhover: { + type: single + on: mouseover + clear: mouseout + empty: none + fields: ["buckets.key", "key"] + nearest: true + } + } + + encoding: { + size: { + condition: { + selection: pointhover + value: 100 + } + value: 5 + } + fill: { + condition: { + selection: pointhover + value: white + } + } + } + }] +} +---- + +==== + +[[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. +This tutorial is not a full https://vega.github.io/vega/tutorials/[Vega tutorial], +but will cover the basics of creating Vega visualizations into {kib}. + +First, create an almost-blank Vega chart by pasting this into the editor: + +```yaml +{ + $schema: "https://vega.github.io/schema/vega/v5.json" + data: [{ + name: source_0 + }] + + scales: [{ + name: x + type: time + range: width + }, { + name: y + type: linear + range: height + }] + + axes: [{ + orient: bottom + scale: x + }, { + orient: left + scale: y + }] + + marks: [ + { + type: area + from: { + data: source_0 + } + encode: { + update: { + } + } + } + ] +} +``` + +Despite being almost blank, this Vega spec still contains the minimum requirements: + +* Data +* Scales +* Marks +* (optional) Axes + +Next, add a valid {es} search query in the `data` block: + +```yaml + data: [ + { + name: source_0 + url: { + %context%: true + %timefield%: order_date + index: kibana_sample_data_ecommerce + body: { + aggs: { + time_buckets: { + date_histogram: { + field: order_date + fixed_interval: "3h" + extended_bounds: { + min: {%timefilter%: "min"} + max: {%timefilter%: "max"} + } + min_doc_count: 0 + } + } + } + size: 0 + } + } + format: { property: "aggregations.time_buckets.buckets" } + } + ] +``` + +Click "Update", and nothing will change in the visualization. The first step +is to change the X and Y scales based on the data: + +```yaml + scales: [{ + name: x + type: time + range: width + domain: { + data: source_0 + field: key + } + }, { + name: y + type: linear + range: height + domain: { + data: source_0 + field: doc_count + } + }] +``` + +Click "Update", and you will see that the X and Y axes are now showing labels based +on the real data. + +Next, encode the fields `key` and `doc_count` as the X and Y values: + +```yaml + marks: [ + { + type: area + from: { + data: source_0 + } + encode: { + update: { + x: { + scale: x + field: key + } + y: { + scale: y + value: 0 + } + y2: { + scale: y + field: doc_count + } + } + } + } + ] +``` + +Click "Update" and you will get a basic area chart: + +[role="screenshot"] +image::visualize/images/vega_tutorial_3.png[] + +Next, add a new block to the `marks` section. This will show clickable points to filter for a specific +date: + +```yaml + { + name: point + type: symbol + style: ["point"] + from: { + data: source_0 + } + encode: { + update: { + x: { + scale: x + field: key + } + y: { + scale: y + field: doc_count + } + size: { + value: 100 + } + fill: { + value: black + } + } + } + } +``` + +Next, we will create a Vega signal to make the points clickable. You can access +the clicked `datum` in the expression used to update. In this case, you want +clicks on points to add a time filter with the 3-hour interval defined above. + +```yaml + signals: [ + { + name: point_click + on: [{ + events: { + source: scope + type: click + markname: point + } + update: '''kibanaSetTimeFilter(datum.key, datum.key + 3 * 60 * 60 * 1000)''' + }] + } + ] +``` + +This event is using the {kib} custom function `kibanaSetTimeFilter` to generate a filter that +gets applied to the entire dashboard on click. + +The mouse cursor does not currently indicate that the chart is interactive. Find the `marks` section, +and update the mark named `point` by adding `cursor: { value: "pointer" }` to +the `encoding` section like this: + +```yaml + { + name: point + type: symbol + style: ["point"] + from: { + data: source_0 + } + encode: { + update: { + ... + cursor: { value: "pointer" } + } + } + } +``` + +Next, we will add a drag interaction which will allow the user to narrow into +a specific time range in the visualization. This will require adding more signals, and +adding a rectangle overlay: + +[role="screenshot"] +image::visualize/images/vega_tutorial_4.png[] + +The first step is to add a new `signal` to track the X position of the cursor: + +```yaml + { + name: currentX + value: -1 + on: [{ + events: { + type: mousemove + source: view + }, + update: "clamp(x(), 0, width)" + }, { + events: { + type: mouseout + source: view + } + update: "-1" + }] + } +``` + +Now add a new `mark` to indicate the current cursor position: + +```yaml + { + type: rule + interactive: false + encode: { + update: { + y: {value: 0} + y2: {signal: "height"} + stroke: {value: "gray"} + strokeDash: { + value: [2, 1] + } + x: {signal: "max(currentX,0)"} + defined: {signal: "currentX > 0"} + } + } + } +``` + +Next, add a signal to track the current selected range, which will update +until the user releases the mouse button or uses the escape key: + + +```yaml + { + name: selected + value: [0, 0] + on: [{ + events: { + type: mousedown + source: view + } + update: "[clamp(x(), 0, width), clamp(x(), 0, width)]" + }, { + events: { + type: mousemove + source: window + consume: true + between: [{ + type: mousedown + source: view + }, { + merge: [{ + type: mouseup + source: window + }, { + type: keydown + source: window + filter: "event.key === 'Escape'" + }] + }] + } + update: "[selected[0], clamp(x(), 0, width)]" + }, { + events: { + type: keydown + source: window + filter: "event.key === 'Escape'" + } + update: "[0, 0]" + }] + } +``` + +Now that there is a signal which tracks the time range from the user, we need to indicate +the range visually by adding a new mark which only appears conditionally: + +```yaml + { + type: rect + name: selectedRect + encode: { + update: { + height: {signal: "height"} + fill: {value: "#333"} + fillOpacity: {value: 0.2} + x: {signal: "selected[0]"} + x2: {signal: "selected[1]"} + defined: {signal: "selected[0] !== selected[1]"} + } + } + } +``` + +Finally, add a new signal which will update the {kib} time filter when the mouse is released while +dragging: + +```yaml + { + name: applyTimeFilter + value: null + on: [{ + events: { + type: mouseup + source: view + } + update: '''selected[0] !== selected[1] ? kibanaSetTimeFilter( + invert('x',selected[0]), + invert('x',selected[1])) : null''' + }] + } +``` + +Putting this all together, your visualization now supports the main features of +standard visualizations in {kib}, but with the potential to add even more control. +The final Vega spec for this tutorial is here: + +.Expand final Vega spec +[%collapsible%closed] +==== +[source,yaml] +---- +{ + $schema: "https://vega.github.io/schema/vega/v5.json" + data: [ + { + name: source_0 + url: { + %context%: true + %timefield%: order_date + index: kibana_sample_data_ecommerce + body: { + aggs: { + time_buckets: { + date_histogram: { + field: order_date + fixed_interval: "3h" + extended_bounds: { + min: {%timefilter%: "min"} + max: {%timefilter%: "max"} + } + min_doc_count: 0 + } + } + } + size: 0 + } + } + format: { property: "aggregations.time_buckets.buckets" } + } + ] + + scales: [{ + name: x + type: time + range: width + domain: { + data: source_0 + field: key + } + }, { + name: y + type: linear + range: height + domain: { + data: source_0 + field: doc_count + } + }] + + axes: [{ + orient: bottom + scale: x + }, { + orient: left + scale: y + }] + + marks: [ + { + type: area + from: { + data: source_0 + } + encode: { + update: { + x: { + scale: x + field: key + } + y: { + scale: y + value: 0 + } + y2: { + scale: y + field: doc_count + } + } + } + }, + { + name: point + type: symbol + style: ["point"] + from: { + data: source_0 + } + encode: { + update: { + x: { + scale: x + field: key + } + y: { + scale: y + field: doc_count + } + size: { + value: 100 + } + fill: { + value: black + } + cursor: { value: "pointer" } + } + } + }, + { + type: rule + interactive: false + encode: { + update: { + y: {value: 0} + y2: {signal: "height"} + stroke: {value: "gray"} + strokeDash: { + value: [2, 1] + } + x: {signal: "max(currentX,0)"} + defined: {signal: "currentX > 0"} + } + } + }, + { + type: rect + name: selectedRect + encode: { + update: { + height: {signal: "height"} + fill: {value: "#333"} + fillOpacity: {value: 0.2} + x: {signal: "selected[0]"} + x2: {signal: "selected[1]"} + defined: {signal: "selected[0] !== selected[1]"} + } + } + } + ] + + signals: [ + { + name: point_click + on: [{ + events: { + source: scope + type: click + markname: point + } + update: '''kibanaSetTimeFilter(datum.key, datum.key + 3 * 60 * 60 * 1000)''' + }] + } + { + name: currentX + value: -1 + on: [{ + events: { + type: mousemove + source: view + }, + update: "clamp(x(), 0, width)" + }, { + events: { + type: mouseout + source: view + } + update: "-1" + }] + } + { + name: selected + value: [0, 0] + on: [{ + events: { + type: mousedown + source: view + } + update: "[clamp(x(), 0, width), clamp(x(), 0, width)]" + }, { + events: { + type: mousemove + source: window + consume: true + between: [{ + type: mousedown + source: view + }, { + merge: [{ + type: mouseup + source: window + }, { + type: keydown + source: window + filter: "event.key === 'Escape'" + }] + }] + } + update: "[selected[0], clamp(x(), 0, width)]" + }, { + events: { + type: keydown + source: window + filter: "event.key === 'Escape'" + } + update: "[0, 0]" + }] + } + { + name: applyTimeFilter + value: null + on: [{ + events: { + type: mouseup + source: view + } + update: '''selected[0] !== selected[1] ? kibanaSetTimeFilter( + invert('x',selected[0]), + invert('x',selected[1])) : null''' + }] + } + ] +} + +---- +==== + +[[timelion-tutorial-create-time-series-visualizations]] +=== Create time series visualizations with Timelion + +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] +[[define-the-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] +[[compare-the-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] +[[add-label-names]] +==== 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] +[[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. + +[role="screenshot"] +image::images/timelion-customize01.png[] +{nbsp} + +[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`. + +[role="screenshot"] +image::images/timelion-customize02.png[] +{nbsp} + +[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. + +[role="screenshot"] +image::images/timelion-customize03.png[] +{nbsp} + +[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. + +[role="screenshot"] +image::images/timelion-customize04.png[] +{nbsp} + +[[timelion-tutorial-create-visualizations-with-mathematical-functions]] +=== Timelion tutorial: 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} + +[[timelion-tutorial-create-visualizations-withconditional-logic-and-tracking-trends]] +=== Create visualizations with conditional logic and tracking trends using Timelion + +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()]. \ 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/docs/visualize/vega.asciidoc b/docs/visualize/vega.asciidoc deleted file mode 100644 index b231159e86bde..0000000000000 --- a/docs/visualize/vega.asciidoc +++ /dev/null @@ -1,1639 +0,0 @@ -[[vega-graph]] -== 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 Vega to embed Vega into -your dashboard, and to add interactivity to the visualizations. - -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. - -[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 - -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 -for a more comprehensive -https://vega.github.io/vega-lite/tutorials/getting_started.html[introduction to Vega-Lite], -while only covering the basics. - -In this tutorial, you will build a stacked area chart from one of the {kib} sample data -sets. - -[role="screenshot"] -image::visualize/images/vega_lite_tutorial_1.png[] - -Before beginning this tutorial, install the <> -set. - -When you first open the Vega editor in {kib}, you will see a pre-populated -line chart which shows the total number of documents across all your indices -within the time range. - -[role="screenshot"] -image::visualize/images/vega_lite_default.png[] - -The text editor contains a Vega-Lite spec written in https://hjson.github.io/[HJSON], -which is similar to JSON but optimized for human editing. HJSON supports: - -* Comments using // or /* syntax -* Object keys without quotes -* String values without quotes -* Optional commas -* Double or single quotes -* Multiline strings - -[float] -==== Small steps - -Always work on Vega in the smallest steps possible, and save your work frequently. -Small changes will cause unexpected results. Click the "Save" button now. - -The first step is to change the index to one of the <> -sets. Change - -```yaml -index: _all -``` - -to: - -```yaml -index: kibana_sample_data_ecommerce -``` - -Click "Update". The result is probably not what you expect. You should see a flat -line with 0 results. - -You've only changed the index, so the difference must be the query is returning -no results. You can try the <>, -but intuition may be faster for this particular problem. - -In this case, the problem is that you are querying the field `@timestamp`, -which does not exist in the `kibana_sample_data_ecommerce` data. Find and replace -`@timestamp` with `order_date`. This fixes the problem, leaving you with this spec: - -.Expand Vega-Lite spec -[%collapsible%closed] -==== -[source,yaml] ----- -{ - $schema: https://vega.github.io/schema/vega-lite/v4.json - title: Event counts from ecommerce - data: { - url: { - %context%: true - %timefield%: order_date - index: kibana_sample_data_ecommerce - body: { - aggs: { - time_buckets: { - date_histogram: { - field: order_date - interval: {%autointerval%: true} - extended_bounds: { - min: {%timefilter%: "min"} - max: {%timefilter%: "max"} - } - min_doc_count: 0 - } - } - } - size: 0 - } - } - format: {property: "aggregations.time_buckets.buckets" } - } - - mark: line - - encoding: { - x: { - field: key - type: temporal - axis: { title: null } - } - y: { - field: doc_count - type: quantitative - axis: { title: "Document count" } - } - } -} ----- - -==== - -Now, let's make the visualization more interesting by adding another aggregation -to create a stacked area chart. To verify that you have constructed the right -query, it is easiest to use the {kib} Dev Tools in a separate tab from the -Vega editor. Open the Dev Tools from the Management section of the navigation. - -This query is roughly equivalent to the one that is used in the default -Vega-Lite spec. Copy it into the Dev Tools: - -```js -POST kibana_sample_data_ecommerce/_search -{ - "query": { - "range": { - "order_date": { - "gte": "now-7d" - } - } - }, - "aggs": { - "time_buckets": { - "date_histogram": { - "field": "order_date", - "fixed_interval": "1d", - "extended_bounds": { - "min": "now-7d" - }, - "min_doc_count": 0 - } - } - }, - "size": 0 -} -``` - -There's not enough data to create a stacked bar in the original query, so we -will add a new -{ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation]: - -```js -POST kibana_sample_data_ecommerce/_search -{ - "query": { - "range": { - "order_date": { - "gte": "now-7d" - } - } - }, - "aggs": { - "categories": { - "terms": { "field": "category.keyword" }, - "aggs": { - "time_buckets": { - "date_histogram": { - "field": "order_date", - "fixed_interval": "1d", - "extended_bounds": { - "min": "now-7d" - }, - "min_doc_count": 0 - } - } - } - } - }, - "size": 0 -} -``` - -You'll see that the response format looks different from the previous query: - -```json -{ - "aggregations" : { - "categories" : { - "doc_count_error_upper_bound" : 0, - "sum_other_doc_count" : 0, - "buckets" : [{ - "key" : "Men's Clothing", - "doc_count" : 1661, - "time_buckets" : { - "buckets" : [{ - "key_as_string" : "2020-06-30T00:00:00.000Z", - "key" : 1593475200000, - "doc_count" : 19 - }, { - "key_as_string" : "2020-07-01T00:00:00.000Z", - "key" : 1593561600000, - "doc_count" : 71 - }] - } - }] - } - } -} -``` - -Now that we have data that we're happy with, it's time to convert from an -isolated {es} query into a query with {kib} integration. Looking at the -<>, you will -see the full list of special tokens that are used in this query, such -as `%context: true`. This query has also replaced `"fixed_interval": "1d"` -with `interval: {%autointerval%: true}`. Copy the final query into -your spec: - -```yaml - data: { - url: { - %context%: true - %timefield%: order_date - index: kibana_sample_data_ecommerce - body: { - aggs: { - categories: { - terms: { field: "category.keyword" } - aggs: { - time_buckets: { - date_histogram: { - field: order_date - interval: {%autointerval%: true} - extended_bounds: { - min: {%timefilter%: "min"} - max: {%timefilter%: "max"} - } - min_doc_count: 0 - } - } - } - } - } - size: 0 - } - } - format: {property: "aggregations.categories.buckets" } - } -``` - -If you copy and paste that into your Vega-Lite spec, and click "Update", -you will see a warning saying `Infinite extent for field "key": [Infinity, -Infinity]`. -Let's use our <> to understand why. - -Vega-Lite generates data using the names `source_0` and `data_0`. `source_0` contains -the results from the {es} query, and `data_0` contains the visually encoded results -which are shown in the chart. To debug this problem, you need to compare both. - -To look at the source, open the browser dev tools console and type -`VEGA_DEBUG.view.data('source_0')`. You will see: - -```js -[{ - doc_count: 454 - key: "Men's Clothing" - time_buckets: {buckets: Array(57)} - Symbol(vega_id): 12822 -}, ...] -``` - -To compare to the visually encoded data, open the browser dev tools console and type -`VEGA_DEBUG.view.data('data_0')`. You will see: - -```js -[{ - doc_count: 454 - key: NaN - time_buckets: {buckets: Array(57)} - Symbol(vega_id): 13879 -}] -``` - -The issue seems to be that the `key` property is not being converted the right way, -which makes sense because the `key` is now `Men's Clothing` instead of a timestamp. - -To fix this, try updating the `encoding` of your Vega-Lite spec to: - -```yaml - encoding: { - x: { - field: time_buckets.buckets.key - type: temporal - axis: { title: null } - } - y: { - field: time_buckets.buckets.doc_count - type: quantitative - axis: { title: "Document count" } - } - } -``` - -This will show more errors, and you can inspect `VEGA_DEBUG.view.data('data_0')` to -understand why. This now shows: - -```js -[{ - doc_count: 454 - key: "Men's Clothing" - time_buckets: {buckets: Array(57)} - time_buckets.buckets.doc_count: undefined - time_buckets.buckets.key: null - Symbol(vega_id): 14094 -}] -``` - -It looks like the problem is that the `time_buckets` inner array is not being -extracted by Vega. The solution is to use a Vega-lite -https://vega.github.io/vega-lite/docs/flatten.html[flatten transformation], available in {kib} 7.9 and later. -If using an older version of Kibana, the flatten transformation is available in Vega -but not Vega-Lite. - -Add this section in between the `data` and `encoding` section: - -```yaml - transform: [{ - flatten: ["time_buckets.buckets"] - }] -``` - -This does not yet produce the results you expect. Inspect the transformed data -by typing `VEGA_DEBUG.view.data('data_0')` into the console again: - -```js -[{ - doc_count: 453 - key: "Men's Clothing" - time_bucket.buckets.doc_count: undefined - time_buckets: {buckets: Array(57)} - time_buckets.buckets: { - key_as_string: "2020-06-30T15:00:00.000Z", - key: 1593529200000, - doc_count: 2 - } - time_buckets.buckets.key: null - Symbol(vega_id): 21564 -}] -``` - -The debug view shows `undefined` values where you would expect to see numbers, and -the cause is that there are duplicate names which are confusing Vega-Lite. This can -be fixed by making this change to the `transform` and `encoding` blocks: - -```yaml - transform: [{ - flatten: ["time_buckets.buckets"], - as: ["buckets"] - }] - - mark: area - - encoding: { - x: { - field: buckets.key - type: temporal - axis: { title: null } - } - y: { - field: buckets.doc_count - type: quantitative - axis: { title: "Document count" } - } - color: { - field: key - type: nominal - } - } -``` - -At this point, you have a stacked area chart that shows the top categories, -but the chart is still missing some common features that we expect from a {kib} -visualization. Let's add hover states and tooltips next. - -Hover states are handled differently in Vega-Lite and Vega. In Vega-Lite this is -done using a concept called `selection`, which has many permutations that are not -covered in this tutorial. We will be adding a simple tooltip and hover state. - -Because {kib} has enabled the https://vega.github.io/vega-lite/docs/tooltip.html[Vega tooltip plugin], -tooltips can be defined in several ways: - -* Automatic tooltip based on the data, via `{ content: "data" }` -* Array of fields, like `[{ field: "key", type: "nominal" }]` -* Defining a custom Javascript object using the `calculate` transform - -For the simple tooltip, add this to your encoding: - -```yaml - encoding: { - tooltip: [{ - field: buckets.key - type: temporal - title: "Date" - }, { - field: key - type: nominal - title: "Category" - }, { - field: buckets.doc_count - type: quantitative - title: "Count" - }] - } -``` - -As you hover over the area series in your chart, a multi-line tooltip will -appear, but it won't indicate the nearest point that it's pointing to. To -indicate the nearest point, we need to add a second layer. - -The first step is to remove the `mark: area` from your visualization. -Once you've removed the previous mark, add a composite mark at the end of -the Vega-Lite spec: - -```yaml - layer: [{ - mark: area - }, { - mark: point - }] -``` - -You'll see that the points are not appearing to line up with the area chart, -and the reason is that the points are not being stacked. Change your Y encoding -to this: - -```yaml - y: { - field: buckets.doc_count - type: quantitative - axis: { title: "Document count" } - stack: true - } -``` - -Now, we will add a `selection` block inside the point mark: - -```yaml - layer: [{ - mark: area - }, { - mark: point - - selection: { - pointhover: { - type: single - on: mouseover - clear: mouseout - empty: none - fields: ["buckets.key", "key"] - nearest: true - } - } - - encoding: { - size: { - condition: { - selection: pointhover - value: 100 - } - value: 5 - } - fill: { - condition: { - selection: pointhover - value: white - } - } - } - }] -``` - -Now that you've enabled a selection, try moving the mouse around the visualization -and seeing the points respond to the nearest position: - -[role="screenshot"] -image::visualize/images/vega_lite_tutorial_2.png[] - -The final result of this tutorial is this spec: - -.Expand final Vega-Lite spec -[%collapsible%closed] -==== -[source,yaml] ----- -{ - $schema: https://vega.github.io/schema/vega-lite/v4.json - title: Event counts from ecommerce - data: { - url: { - %context%: true - %timefield%: order_date - index: kibana_sample_data_ecommerce - body: { - aggs: { - categories: { - terms: { field: "category.keyword" } - aggs: { - time_buckets: { - date_histogram: { - field: order_date - interval: {%autointerval%: true} - extended_bounds: { - min: {%timefilter%: "min"} - max: {%timefilter%: "max"} - } - min_doc_count: 0 - } - } - } - } - } - size: 0 - } - } - format: {property: "aggregations.categories.buckets" } - } - - transform: [{ - flatten: ["time_buckets.buckets"] - as: ["buckets"] - }] - - encoding: { - x: { - field: buckets.key - type: temporal - axis: { title: null } - } - y: { - field: buckets.doc_count - type: quantitative - axis: { title: "Document count" } - stack: true - } - color: { - field: key - type: nominal - title: "Category" - } - tooltip: [{ - field: buckets.key - type: temporal - title: "Date" - }, { - field: key - type: nominal - title: "Category" - }, { - field: buckets.doc_count - type: quantitative - title: "Count" - }] - } - - layer: [{ - mark: area - }, { - mark: point - - selection: { - pointhover: { - type: single - on: mouseover - clear: mouseout - empty: none - fields: ["buckets.key", "key"] - nearest: true - } - } - - encoding: { - size: { - condition: { - selection: pointhover - value: 100 - } - value: 5 - } - fill: { - condition: { - selection: pointhover - value: white - } - } - } - }] -} ----- - -==== - -[[vega-tutorial]] -=== Tutorial: Updating {kib} filters from Vega - -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. -This tutorial is not a full https://vega.github.io/vega/tutorials/[Vega tutorial], -but will cover the basics of creating Vega visualizations into {kib}. - -First, create an almost-blank Vega chart by pasting this into the editor: - -```yaml -{ - $schema: "https://vega.github.io/schema/vega/v5.json" - data: [{ - name: source_0 - }] - - scales: [{ - name: x - type: time - range: width - }, { - name: y - type: linear - range: height - }] - - axes: [{ - orient: bottom - scale: x - }, { - orient: left - scale: y - }] - - marks: [ - { - type: area - from: { - data: source_0 - } - encode: { - update: { - } - } - } - ] -} -``` - -Despite being almost blank, this Vega spec still contains the minimum requirements: - -* Data -* Scales -* Marks -* (optional) Axes - -Next, add a valid {es} search query in the `data` block: - -```yaml - data: [ - { - name: source_0 - url: { - %context%: true - %timefield%: order_date - index: kibana_sample_data_ecommerce - body: { - aggs: { - time_buckets: { - date_histogram: { - field: order_date - fixed_interval: "3h" - extended_bounds: { - min: {%timefilter%: "min"} - max: {%timefilter%: "max"} - } - min_doc_count: 0 - } - } - } - size: 0 - } - } - format: { property: "aggregations.time_buckets.buckets" } - } - ] -``` - -Click "Update", and nothing will change in the visualization. The first step -is to change the X and Y scales based on the data: - -```yaml - scales: [{ - name: x - type: time - range: width - domain: { - data: source_0 - field: key - } - }, { - name: y - type: linear - range: height - domain: { - data: source_0 - field: doc_count - } - }] -``` - -Click "Update", and you will see that the X and Y axes are now showing labels based -on the real data. - -Next, encode the fields `key` and `doc_count` as the X and Y values: - -```yaml - marks: [ - { - type: area - from: { - data: source_0 - } - encode: { - update: { - x: { - scale: x - field: key - } - y: { - scale: y - value: 0 - } - y2: { - scale: y - field: doc_count - } - } - } - } - ] -``` - -Click "Update" and you will get a basic area chart: - -[role="screenshot"] -image::visualize/images/vega_tutorial_3.png[] - -Next, add a new block to the `marks` section. This will show clickable points to filter for a specific -date: - -```yaml - { - name: point - type: symbol - style: ["point"] - from: { - data: source_0 - } - encode: { - update: { - x: { - scale: x - field: key - } - y: { - scale: y - field: doc_count - } - size: { - value: 100 - } - fill: { - value: black - } - } - } - } -``` - -Next, we will create a Vega signal to make the points clickable. You can access -the clicked `datum` in the expression used to update. In this case, you want -clicks on points to add a time filter with the 3-hour interval defined above. - -```yaml - signals: [ - { - name: point_click - on: [{ - events: { - source: scope - type: click - markname: point - } - update: '''kibanaSetTimeFilter(datum.key, datum.key + 3 * 60 * 60 * 1000)''' - }] - } - ] -``` - -This event is using the {kib} custom function `kibanaSetTimeFilter` to generate a filter that -gets applied to the entire dashboard on click. - -The mouse cursor does not currently indicate that the chart is interactive. Find the `marks` section, -and update the mark named `point` by adding `cursor: { value: "pointer" }` to -the `encoding` section like this: - -```yaml - { - name: point - type: symbol - style: ["point"] - from: { - data: source_0 - } - encode: { - update: { - ... - cursor: { value: "pointer" } - } - } - } -``` - -Next, we will add a drag interaction which will allow the user to narrow into -a specific time range in the visualization. This will require adding more signals, and -adding a rectangle overlay: - -[role="screenshot"] -image::visualize/images/vega_tutorial_4.png[] - -The first step is to add a new `signal` to track the X position of the cursor: - -```yaml - { - name: currentX - value: -1 - on: [{ - events: { - type: mousemove - source: view - }, - update: "clamp(x(), 0, width)" - }, { - events: { - type: mouseout - source: view - } - update: "-1" - }] - } -``` - -Now add a new `mark` to indicate the current cursor position: - -```yaml - { - type: rule - interactive: false - encode: { - update: { - y: {value: 0} - y2: {signal: "height"} - stroke: {value: "gray"} - strokeDash: { - value: [2, 1] - } - x: {signal: "max(currentX,0)"} - defined: {signal: "currentX > 0"} - } - } - } -``` - -Next, add a signal to track the current selected range, which will update -until the user releases the mouse button or uses the escape key: - - -```yaml - { - name: selected - value: [0, 0] - on: [{ - events: { - type: mousedown - source: view - } - update: "[clamp(x(), 0, width), clamp(x(), 0, width)]" - }, { - events: { - type: mousemove - source: window - consume: true - between: [{ - type: mousedown - source: view - }, { - merge: [{ - type: mouseup - source: window - }, { - type: keydown - source: window - filter: "event.key === 'Escape'" - }] - }] - } - update: "[selected[0], clamp(x(), 0, width)]" - }, { - events: { - type: keydown - source: window - filter: "event.key === 'Escape'" - } - update: "[0, 0]" - }] - } -``` - -Now that there is a signal which tracks the time range from the user, we need to indicate -the range visually by adding a new mark which only appears conditionally: - -```yaml - { - type: rect - name: selectedRect - encode: { - update: { - height: {signal: "height"} - fill: {value: "#333"} - fillOpacity: {value: 0.2} - x: {signal: "selected[0]"} - x2: {signal: "selected[1]"} - defined: {signal: "selected[0] !== selected[1]"} - } - } - } -``` - -Finally, add a new signal which will update the {kib} time filter when the mouse is released while -dragging: - -```yaml - { - name: applyTimeFilter - value: null - on: [{ - events: { - type: mouseup - source: view - } - update: '''selected[0] !== selected[1] ? kibanaSetTimeFilter( - invert('x',selected[0]), - invert('x',selected[1])) : null''' - }] - } -``` - -Putting this all together, your visualization now supports the main features of -standard visualizations in {kib}, but with the potential to add even more control. -The final Vega spec for this tutorial is here: - -.Expand final Vega spec -[%collapsible%closed] -==== -[source,yaml] ----- -{ - $schema: "https://vega.github.io/schema/vega/v5.json" - data: [ - { - name: source_0 - url: { - %context%: true - %timefield%: order_date - index: kibana_sample_data_ecommerce - body: { - aggs: { - time_buckets: { - date_histogram: { - field: order_date - fixed_interval: "3h" - extended_bounds: { - min: {%timefilter%: "min"} - max: {%timefilter%: "max"} - } - min_doc_count: 0 - } - } - } - size: 0 - } - } - format: { property: "aggregations.time_buckets.buckets" } - } - ] - - scales: [{ - name: x - type: time - range: width - domain: { - data: source_0 - field: key - } - }, { - name: y - type: linear - range: height - domain: { - data: source_0 - field: doc_count - } - }] - - axes: [{ - orient: bottom - scale: x - }, { - orient: left - scale: y - }] - - marks: [ - { - type: area - from: { - data: source_0 - } - encode: { - update: { - x: { - scale: x - field: key - } - y: { - scale: y - value: 0 - } - y2: { - scale: y - field: doc_count - } - } - } - }, - { - name: point - type: symbol - style: ["point"] - from: { - data: source_0 - } - encode: { - update: { - x: { - scale: x - field: key - } - y: { - scale: y - field: doc_count - } - size: { - value: 100 - } - fill: { - value: black - } - cursor: { value: "pointer" } - } - } - }, - { - type: rule - interactive: false - encode: { - update: { - y: {value: 0} - y2: {signal: "height"} - stroke: {value: "gray"} - strokeDash: { - value: [2, 1] - } - x: {signal: "max(currentX,0)"} - defined: {signal: "currentX > 0"} - } - } - }, - { - type: rect - name: selectedRect - encode: { - update: { - height: {signal: "height"} - fill: {value: "#333"} - fillOpacity: {value: 0.2} - x: {signal: "selected[0]"} - x2: {signal: "selected[1]"} - defined: {signal: "selected[0] !== selected[1]"} - } - } - } - ] - - signals: [ - { - name: point_click - on: [{ - events: { - source: scope - type: click - markname: point - } - update: '''kibanaSetTimeFilter(datum.key, datum.key + 3 * 60 * 60 * 1000)''' - }] - } - { - name: currentX - value: -1 - on: [{ - events: { - type: mousemove - source: view - }, - update: "clamp(x(), 0, width)" - }, { - events: { - type: mouseout - source: view - } - update: "-1" - }] - } - { - name: selected - value: [0, 0] - on: [{ - events: { - type: mousedown - source: view - } - update: "[clamp(x(), 0, width), clamp(x(), 0, width)]" - }, { - events: { - type: mousemove - source: window - consume: true - between: [{ - type: mousedown - source: view - }, { - merge: [{ - type: mouseup - source: window - }, { - type: keydown - source: window - filter: "event.key === 'Escape'" - }] - }] - } - update: "[selected[0], clamp(x(), 0, width)]" - }, { - events: { - type: keydown - source: window - filter: "event.key === 'Escape'" - } - update: "[0, 0]" - }] - } - { - name: applyTimeFilter - value: null - on: [{ - events: { - type: mouseup - source: view - } - update: '''selected[0] !== selected[1] ? kibanaSetTimeFilter( - invert('x',selected[0]), - invert('x',selected[1])) : null''' - }] - } - ] -} - ----- -==== - -[[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 - -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. - -[[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. - -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 - -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 - } - } - } -} -``` - -[[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}. - -[[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*. - -===== Inspect Elasticsearch 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[] - -===== 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 <>. - -===== 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. - -[[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]] -[[vega-useful-links]] -=== Resources and examples - -To learn more about Vega and Vega-Lite, refer to the resources and examples. - -==== 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. - -==== 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] - -==== 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/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/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index b033fe86cd1c7..48b81c27e8b8d 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -71,11 +71,7 @@ export class BookEmbeddable constructor( initialInput: BookEmbeddableInput, - private attributeService: AttributeService< - BookSavedObjectAttributes, - BookByValueInput, - BookByReferenceInput - >, + private attributeService: AttributeService, { parent, }: { @@ -99,18 +95,21 @@ export class BookEmbeddable }); } - inputIsRefType = (input: BookEmbeddableInput): input is BookByReferenceInput => { + readonly inputIsRefType = (input: BookEmbeddableInput): input is BookByReferenceInput => { return this.attributeService.inputIsRefType(input); }; - getInputAsValueType = async (): Promise => { + readonly getInputAsValueType = async (): Promise => { const input = this.attributeService.getExplicitInputFromEmbeddable(this); return this.attributeService.getInputAsValueType(input); }; - getInputAsRefType = async (): Promise => { + readonly getInputAsRefType = async (): Promise => { const input = this.attributeService.getExplicitInputFromEmbeddable(this); - return this.attributeService.getInputAsRefType(input, { showSaveModal: true }); + return this.attributeService.getInputAsRefType(input, { + showSaveModal: true, + saveModalTitle: this.getTitle(), + }); }; public render(node: HTMLElement) { diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx index 4c144c3843c47..292261ee16c59 100644 --- a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx @@ -31,8 +31,6 @@ import { BOOK_EMBEDDABLE, BookEmbeddableInput, BookEmbeddableOutput, - BookByValueInput, - BookByReferenceInput, } from './book_embeddable'; import { CreateEditBookComponent } from './create_edit_book_component'; import { OverlayStart } from '../../../../src/core/public'; @@ -66,11 +64,7 @@ export class BookEmbeddableFactoryDefinition getIconForSavedObject: () => 'pencil', }; - private attributeService?: AttributeService< - BookSavedObjectAttributes, - BookByValueInput, - BookByReferenceInput - >; + private attributeService?: AttributeService; constructor(private getStartServices: () => Promise) {} @@ -126,9 +120,7 @@ export class BookEmbeddableFactoryDefinition private async getAttributeService() { if (!this.attributeService) { this.attributeService = await (await this.getStartServices()).getAttributeService< - BookSavedObjectAttributes, - BookByValueInput, - BookByReferenceInput + BookSavedObjectAttributes >(this.type); } return this.attributeService!; diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx index 5b14dc85b1fc7..3541ace1e5e7e 100644 --- a/examples/embeddable_examples/public/book/edit_book_action.tsx +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -57,13 +57,13 @@ export const createEditBookAction = (getStartServices: () => Promise { const { openModal, getAttributeService } = await getStartServices(); - const attributeService = getAttributeService< - BookSavedObjectAttributes, - BookByValueInput, - BookByReferenceInput - >(BOOK_SAVED_OBJECT); + const attributeService = getAttributeService(BOOK_SAVED_OBJECT); const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => { - const newInput = await attributeService.wrapAttributes(attributes, useRefType, embeddable); + const newInput = await attributeService.wrapAttributes( + attributes, + useRefType, + attributeService.getExplicitInputFromEmbeddable(embeddable) + ); if (!useRefType && (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId) { // Set the saved object ID to null so that update input will remove the existing savedObjectId... (newInput as BookByValueInput & { savedObjectId: unknown }).savedObjectId = null; 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 b8cf2e1e27774..ff487510f7a32 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "test:ftr:server": "node scripts/functional_tests_server", "test:ftr:runner": "node scripts/functional_test_runner", "test:coverage": "grunt test:coverage", - "typespec": "typings-tester --config x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", "checkLicenses": "node scripts/check_licenses --dev", "build": "node scripts/build --all-platforms", "start": "node scripts/kibana --dev", @@ -66,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", @@ -93,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", @@ -117,7 +117,10 @@ "**/@types/*/**", "**/grunt-*", "**/grunt-*/**", - "x-pack/typescript" + "x-pack/typescript", + "@elastic/eui/rehype-react", + "@elastic/eui/remark-rehype", + "@elastic/eui/remark-rehype/**" ] }, "dependencies": { @@ -125,7 +128,7 @@ "@babel/register": "^7.10.5", "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "7.9.0-rc.2", - "@elastic/eui": "27.4.1", + "@elastic/eui": "28.2.0", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", @@ -142,6 +145,7 @@ "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", "accept": "3.0.2", @@ -188,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", @@ -227,7 +231,7 @@ "@babel/parser": "^7.11.2", "@babel/types": "^7.11.0", "@elastic/apm-rum": "^5.5.0", - "@elastic/charts": "19.8.1", + "@elastic/charts": "21.0.1", "@elastic/ems-client": "7.9.3", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", @@ -302,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", @@ -363,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", @@ -470,7 +473,6 @@ "topojson-client": "3.0.0", "tree-kill": "^1.2.2", "typescript": "4.0.2", - "typings-tester": "^0.3.2", "ui-select": "0.19.8", "vega": "^5.13.0", "vega-lite": "^4.13.1", 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/eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js similarity index 100% rename from packages/eslint-config-kibana/.eslintrc.js rename to packages/elastic-eslint-config-kibana/.eslintrc.js diff --git a/packages/eslint-config-kibana/.gitignore b/packages/elastic-eslint-config-kibana/.gitignore similarity index 100% rename from packages/eslint-config-kibana/.gitignore rename to packages/elastic-eslint-config-kibana/.gitignore diff --git a/packages/eslint-config-kibana/.npmignore b/packages/elastic-eslint-config-kibana/.npmignore similarity index 100% rename from packages/eslint-config-kibana/.npmignore rename to packages/elastic-eslint-config-kibana/.npmignore diff --git a/packages/elastic-eslint-config-kibana/README.md b/packages/elastic-eslint-config-kibana/README.md new file mode 100644 index 0000000000000..2049440cd8ff7 --- /dev/null +++ b/packages/elastic-eslint-config-kibana/README.md @@ -0,0 +1,31 @@ +# elastic-eslint-config-kibana + +The eslint config used by the kibana team + +## Usage + +To use this eslint config, just install the peer dependencies and reference it +in your `.eslintrc`: + +```javascript +{ + extends: [ + '@elastic/eslint-config-kibana' + ] +} +``` + +## Optional jest config + +If the project uses the [jest test runner](https://facebook.github.io/jest/), +the `@elastic/eslint-config-kibana/jest` config can be extended as well to use +`eslint-plugin-jest` and add settings specific to it: + +```javascript +{ + extends: [ + '@elastic/eslint-config-kibana', + '@elastic/eslint-config-kibana/jest' + ] +} +``` diff --git a/packages/eslint-config-kibana/javascript.js b/packages/elastic-eslint-config-kibana/javascript.js similarity index 100% rename from packages/eslint-config-kibana/javascript.js rename to packages/elastic-eslint-config-kibana/javascript.js diff --git a/packages/eslint-config-kibana/jest.js b/packages/elastic-eslint-config-kibana/jest.js similarity index 100% rename from packages/eslint-config-kibana/jest.js rename to packages/elastic-eslint-config-kibana/jest.js diff --git a/packages/elastic-eslint-config-kibana/package.json b/packages/elastic-eslint-config-kibana/package.json new file mode 100644 index 0000000000000..a4bb8d5449ee8 --- /dev/null +++ b/packages/elastic-eslint-config-kibana/package.json @@ -0,0 +1,34 @@ +{ + "name": "@elastic/eslint-config-kibana", + "version": "0.15.0", + "description": "The eslint config used by the kibana team", + "main": ".eslintrc.js", + "repository": { + "type": "git", + "url": "git+https://github.com/elastic/kibana.git" + }, + "keywords": [], + "author": "Spencer Alger ", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/elastic/kibana/tree/master/packages/elastic-eslint-config-kibana" + }, + "homepage": "https://github.com/elastic/kibana/tree/master/packages/elastic-eslint-config-kibana", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^3.10.0", + "@typescript-eslint/parser": "^3.10.0", + "babel-eslint": "^10.0.3", + "eslint": "^6.8.0", + "eslint-plugin-babel": "^5.3.0", + "eslint-plugin-ban": "^1.4.0", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-import": "^2.19.1", + "eslint-plugin-jest": "^23.10.0", + "eslint-plugin-mocha": "^6.2.2", + "eslint-plugin-no-unsanitized": "^3.0.2", + "eslint-plugin-prefer-object-spread": "^1.2.1", + "eslint-plugin-react": "^7.17.0", + "eslint-plugin-react-hooks": "^4.0.4" + } +} diff --git a/packages/eslint-config-kibana/react.js b/packages/elastic-eslint-config-kibana/react.js similarity index 100% rename from packages/eslint-config-kibana/react.js rename to packages/elastic-eslint-config-kibana/react.js diff --git a/packages/eslint-config-kibana/restricted_globals.js b/packages/elastic-eslint-config-kibana/restricted_globals.js similarity index 100% rename from packages/eslint-config-kibana/restricted_globals.js rename to packages/elastic-eslint-config-kibana/restricted_globals.js diff --git a/packages/eslint-config-kibana/typescript.js b/packages/elastic-eslint-config-kibana/typescript.js similarity index 98% rename from packages/eslint-config-kibana/typescript.js rename to packages/elastic-eslint-config-kibana/typescript.js index 18b11eb62beef..d3e80b7448151 100644 --- a/packages/eslint-config-kibana/typescript.js +++ b/packages/elastic-eslint-config-kibana/typescript.js @@ -223,7 +223,8 @@ module.exports = { 'no-undef-init': 'error', 'no-unsafe-finally': 'error', 'no-unsanitized/property': 'error', - 'no-unused-expressions': 'error', + 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-labels': 'error', 'no-var': 'error', 'object-shorthand': 'error', 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.test-d.ts deleted file mode 100644 index ab29d7de5a03f..0000000000000 --- a/packages/elastic-safer-lodash-set/test/index.test-d.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/elastic-safer-lodash-set/LICENSE` for more information. - */ - -import { expectType } from 'tsd'; -import { set, setWith } from '../'; - -const someObj: object = {}; -const anyValue: any = 'any value'; - -expectType(set(someObj, 'a.b.c', anyValue)); -expectType( - setWith(someObj, 'a.b.c', anyValue, (value, key, obj) => { - expectType(value); - expectType(key); - expectType(obj); - }) -); - -expectType(set(someObj, ['a.b.c'], anyValue)); -expectType( - setWith(someObj, ['a.b.c'], anyValue, (value, key, obj) => { - expectType(value); - expectType(key); - expectType(obj); - }) -); - -expectType(set(someObj, ['a.b.c', 2, Symbol('hep')], anyValue)); -expectType( - setWith(someObj, ['a.b.c', 2, Symbol('hep')], anyValue, (value, key, obj) => { - expectType(value); - expectType(key); - expectType(obj); - }) -); diff --git a/packages/elastic-safer-lodash-set/test/index.ts b/packages/elastic-safer-lodash-set/test/index.ts new file mode 100644 index 0000000000000..2090c1adcfce1 --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/index.ts @@ -0,0 +1,37 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { expectType } from 'tsd'; +import { set, setWith } from '..'; + +const someObj: object = {}; +const anyValue: any = 'any value'; + +expectType(set(someObj, 'a.b.c', anyValue)); +expectType( + setWith(someObj, 'a.b.c', anyValue, (value, key, obj) => { + expectType(value); + expectType(key); + expectType(obj); + }) +); + +expectType(set(someObj, ['a.b.c'], anyValue)); +expectType( + setWith(someObj, ['a.b.c'], anyValue, (value, key, obj) => { + expectType(value); + expectType(key); + expectType(obj); + }) +); + +expectType(set(someObj, ['a.b.c', 2, Symbol('hep')], anyValue)); +expectType( + setWith(someObj, ['a.b.c', 2, Symbol('hep')], anyValue, (value, key, obj) => { + expectType(value); + expectType(key); + expectType(obj); + }) +); 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/eslint-config-kibana/README.md b/packages/eslint-config-kibana/README.md deleted file mode 100644 index 68c1639b834a5..0000000000000 --- a/packages/eslint-config-kibana/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# eslint-config-kibana - -The eslint config used by the kibana team - -## Usage - -To use this eslint config, just install the peer dependencies and reference it -in your `.eslintrc`: - -```javascript -{ - extends: [ - '@elastic/eslint-config-kibana' - ] -} -``` - -## Optional jest config - -If the project uses the [jest test runner](https://facebook.github.io/jest/), -the `@elastic/eslint-config-kibana/jest` config can be extended as well to use -`eslint-plugin-jest` and add settings specific to it: - -```javascript -{ - extends: [ - '@elastic/eslint-config-kibana', - '@elastic/eslint-config-kibana/jest' - ] -} -``` diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json deleted file mode 100644 index 4ec3bcdfd7c05..0000000000000 --- a/packages/eslint-config-kibana/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@elastic/eslint-config-kibana", - "version": "0.15.0", - "description": "The eslint config used by the kibana team", - "main": ".eslintrc.js", - "repository": { - "type": "git", - "url": "git+https://github.com/elastic/eslint-config-kibana.git" - }, - "keywords": [], - "author": "Spencer Alger ", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/elastic/kibana/tree/master/packages/eslint-config-kibana" - }, - "homepage": "https://github.com/elastic/kibana/tree/master/packages/eslint-config-kibana", - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^3.10.0", - "@typescript-eslint/parser": "^3.10.0", - "babel-eslint": "^10.0.3", - "eslint": "^6.8.0", - "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-ban": "^1.4.0", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.19.1", - "eslint-plugin-jest": "^23.10.0", - "eslint-plugin-mocha": "^6.2.2", - "eslint-plugin-no-unsanitized": "^3.0.2", - "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-react": "^7.17.0", - "eslint-plugin-react-hooks": "^4.0.4" - } -} 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/packages/kbn-es-archiver/src/lib/streams.ts b/packages/kbn-es-archiver/src/lib/streams.ts deleted file mode 100644 index a90afbe0c4d25..0000000000000 --- a/packages/kbn-es-archiver/src/lib/streams.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 * from '../../../../src/legacy/utils/streams'; 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/packages/kbn-es-archiver/src/lib/streams/concat_stream.ts b/packages/kbn-es-archiver/src/lib/streams/concat_stream.ts new file mode 100644 index 0000000000000..03dd894067afc --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/concat_stream.ts @@ -0,0 +1,41 @@ +/* + * 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 { createReduceStream } from './reduce_stream'; + +/** + * 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/packages/kbn-es-archiver/src/lib/streams/filter_stream.ts b/packages/kbn-es-archiver/src/lib/streams/filter_stream.ts new file mode 100644 index 0000000000000..738b9d5793d06 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/filter_stream.ts @@ -0,0 +1,33 @@ +/* + * 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 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/packages/kbn-es-archiver/src/lib/streams/list_stream.ts b/packages/kbn-es-archiver/src/lib/streams/list_stream.ts new file mode 100644 index 0000000000000..c061b969b3c09 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/list_stream.ts @@ -0,0 +1,41 @@ +/* + * 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'; + +/** + * 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); + }); + + 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/packages/kbn-es-archiver/src/lib/streams/map_stream.ts b/packages/kbn-es-archiver/src/lib/streams/map_stream.ts new file mode 100644 index 0000000000000..e88c512a38653 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/map_stream.ts @@ -0,0 +1,36 @@ +/* + * 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 createMapStream(fn: (chunk: any, i: number) => T | Promise) { + let i = 0; + + 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/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts b/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts new file mode 100644 index 0000000000000..fefb18be14780 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts @@ -0,0 +1,64 @@ +/* + * 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. + */ + +import { pipeline, Writable } from 'stream'; +import { promisify } from 'util'; + +const asyncPipeline = promisify(pipeline); + +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 + throw new Error('A minimum of 2 streams is required when a non-readable stream is given'); + } + if (typeof last.read === 'function') { + // 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, _, done) { + finalChunk = chunk; + done(); + }, + }) + ); + } + + 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/package.json b/packages/kbn-es/package.json index c3670f648d309..dabf11fdd0b66 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -16,7 +16,7 @@ "glob": "^7.1.2", "node-fetch": "^2.6.0", "simple-git": "^1.91.0", - "tar-fs": "^1.16.3", + "tar-fs": "^2.1.0", "tree-kill": "^1.2.2", "yauzl": "^2.10.0" } 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/package.json b/packages/kbn-plugin-helpers/package.json index 129c58a4b4174..f292387c12521 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -23,10 +23,10 @@ "vinyl-fs": "^3.0.3" }, "devDependencies": { - "@types/decompress": "^4.2.3", + "@types/extract-zip": "^1.6.2", "@types/gulp-zip": "^4.0.1", "@types/inquirer": "^6.5.0", - "decompress": "^4.2.1", + "extract-zip": "^2.0.1", "typescript": "4.0.2" } } diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts index 62f83cd672f3d..be23d8dbde646 100644 --- a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -22,7 +22,7 @@ import Fs from 'fs'; import execa from 'execa'; import { createStripAnsiSerializer, REPO_ROOT, createReplaceSerializer } from '@kbn/dev-utils'; -import decompress from 'decompress'; +import extract from 'extract-zip'; import del from 'del'; import globby from 'globby'; import loadJsonFile from 'load-json-file'; @@ -81,7 +81,7 @@ it('builds a generated plugin into a viable archive', async () => { info compressing plugin into [fooTestPlugin-7.5.0.zip]" `); - await decompress(PLUGIN_ARCHIVE, TMP_DIR); + await extract(PLUGIN_ARCHIVE, { dir: TMP_DIR }); const files = await globby(['**/*'], { cwd: TMP_DIR }); files.sort((a, b) => a.localeCompare(b)); 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/README.md b/packages/kbn-pm/README.md index 05405e7b98374..c169b5c75e178 100644 --- a/packages/kbn-pm/README.md +++ b/packages/kbn-pm/README.md @@ -19,7 +19,7 @@ From a plugin perspective there are two different types of Kibana dependencies: runtime and static dependencies. Runtime dependencies are things that are instantiated at runtime and that are injected into the plugin, for example config and elasticsearch clients. Static dependencies are those dependencies -that we want to `import`. `eslint-config-kibana` is one example of this, and +that we want to `import`. `elastic-eslint-config-kibana` is one example of this, and it's actually needed because eslint requires it to be a separate package. But we also have dependencies like `datemath`, `flot`, `eui` and others that we control, but where we want to `import` them in plugins instead of injecting them 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/package.json b/packages/kbn-test/package.json index 24655f8e57026..c84b0a93311bb 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -32,7 +32,7 @@ "parse-link-header": "^1.0.1", "rxjs": "^6.5.5", "strip-ansi": "^5.2.0", - "tar-fs": "^1.16.3", + "tar-fs": "^2.1.0", "tmp": "^0.1.0", "xml2js": "^0.4.22", "zlib": "^1.0.5" 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 531513481b1d4..4b2e88d155245 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,8 +9,8 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "19.8.1", - "@elastic/eui": "27.4.1", + "@elastic/charts": "21.0.1", + "@elastic/eui": "28.2.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", "@kbn/monaco": "1.0.0", @@ -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/scripts/build_ts_refs.js b/scripts/build_ts_refs.js new file mode 100644 index 0000000000000..29fd66bab4ca9 --- /dev/null +++ b/scripts/build_ts_refs.js @@ -0,0 +1,20 @@ +/* + * 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. + */ +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/__snapshots__/application_service.test.ts.snap b/src/core/public/application/__snapshots__/application_service.test.ts.snap index c63a22170c4f6..a6c9eb27e338a 100644 --- a/src/core/public/application/__snapshots__/application_service.test.ts.snap +++ b/src/core/public/application/__snapshots__/application_service.test.ts.snap @@ -80,6 +80,7 @@ exports[`#start() getComponent returns renderable JSX tree 1`] = ` } } mounters={Map {}} + setAppActionMenu={[Function]} setAppLeaveHandler={[Function]} setIsMounting={[Function]} /> diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index 47a8a01d917eb..5609e7eb5d17d 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -20,6 +20,7 @@ import { History } from 'history'; import { BehaviorSubject, Subject } from 'rxjs'; +import type { MountPoint } from '../types'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; import { ApplicationSetup, @@ -27,7 +28,6 @@ import { ApplicationStart, InternalApplicationSetup, PublicAppInfo, - PublicLegacyAppInfo, } from './types'; import { ApplicationServiceContract } from './test_types'; @@ -39,7 +39,6 @@ const createSetupContractMock = (): jest.Mocked => ({ const createInternalSetupContractMock = (): jest.Mocked => ({ register: jest.fn(), - registerLegacyApp: jest.fn(), registerAppUpdater: jest.fn(), registerMountContext: jest.fn(), }); @@ -48,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(), @@ -84,9 +83,10 @@ 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), getComponent: jest.fn(), getUrlForApp: jest.fn(), navigateToApp: jest.fn().mockImplementation((appId) => currentAppId$.next(appId)), 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 d7f15decb255d..0d08f6f3007b0 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -22,7 +22,7 @@ import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; import { map, shareReplay, takeUntil, distinctUntilChanged, filter } from 'rxjs/operators'; import { createBrowserHistory, History } from 'history'; -import { InjectedMetadataSetup } from '../injected_metadata'; +import { MountPoint } from '../types'; import { HttpSetup, HttpStart } from '../http'; import { OverlayStart } from '../overlays'; import { ContextSetup, IContextContainer } from '../context'; @@ -31,7 +31,6 @@ import { AppRouter } from './ui'; import { Capabilities, CapabilitiesService } from './capabilities'; import { App, - AppBase, AppLeaveHandler, AppMount, AppMountDeprecated, @@ -41,8 +40,6 @@ import { AppUpdater, InternalApplicationSetup, InternalApplicationStart, - LegacyApp, - LegacyAppMounter, Mounter, NavigateToAppOptions, } from './types'; @@ -52,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; } @@ -90,16 +86,22 @@ interface AppUpdaterWrapper { updater: AppUpdater; } +interface AppInternalState { + leaveHandler?: AppLeaveHandler; + actionMenu?: MountPoint; +} + /** * Service that is responsible for registering new applications. * @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 appLeaveHandlers = new Map(); + private readonly appInternalStates = new Map(); private currentAppId$ = new BehaviorSubject(undefined); + private currentActionMenu$ = new BehaviorSubject(undefined); private readonly statusUpdaters$ = new BehaviorSubject>(new Map()); private readonly subscriptions: Subscription[] = []; private stop$ = new Subject(); @@ -112,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; @@ -193,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$); @@ -204,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) => @@ -293,12 +246,14 @@ export class ApplicationService { if (path === undefined) { path = applications$.value.get(appId)?.defaultPath; } - this.appLeaveHandlers.delete(this.currentAppId$.value!); + this.appInternalStates.delete(this.currentAppId$.value!); this.navigate!(getAppUrl(availableMounters, appId, path), state, replace); this.currentAppId$.next(appId); } }; + this.currentAppId$.subscribe(() => this.refreshCurrentActionMenu()); + return { applications$: applications$.pipe( map((apps) => new Map([...apps.entries()].map(([id, app]) => [id, getAppInfo(app)]))), @@ -310,7 +265,11 @@ export class ApplicationService { distinctUntilChanged(), takeUntil(this.stop$) ), - history: this.history, + currentActionMenu$: this.currentActionMenu$.pipe( + distinctUntilChanged(), + takeUntil(this.stop$) + ), + history: this.history!, registerMountContext: this.mountContext.registerContext, getUrlForApp: ( appId, @@ -338,6 +297,7 @@ export class ApplicationService { mounters={availableMounters} appStatuses$={applicationStatuses$} setAppLeaveHandler={this.setAppLeaveHandler} + setAppActionMenu={this.setAppActionMenu} setIsMounting={(isMounting) => httpLoadingCount$.next(isMounting ? 1 : 0)} /> ); @@ -346,7 +306,24 @@ export class ApplicationService { } private setAppLeaveHandler = (appId: string, handler: AppLeaveHandler) => { - this.appLeaveHandlers.set(appId, handler); + this.appInternalStates.set(appId, { + ...(this.appInternalStates.get(appId) ?? {}), + leaveHandler: handler, + }); + }; + + private setAppActionMenu = (appId: string, mount: MountPoint | undefined) => { + this.appInternalStates.set(appId, { + ...(this.appInternalStates.get(appId) ?? {}), + actionMenu: mount, + }); + this.refreshCurrentActionMenu(); + }; + + private refreshCurrentActionMenu = () => { + const appId = this.currentAppId$.getValue(); + const currentActionMenu = appId ? this.appInternalStates.get(appId)?.actionMenu : undefined; + this.currentActionMenu$.next(currentActionMenu); }; private async shouldNavigate(overlays: OverlayStart): Promise { @@ -354,7 +331,7 @@ export class ApplicationService { if (currentAppId === undefined) { return true; } - const action = getLeaveAction(this.appLeaveHandlers.get(currentAppId)); + const action = getLeaveAction(this.appInternalStates.get(currentAppId)?.leaveHandler); if (isConfirmAction(action)) { const confirmed = await overlays.openConfirm(action.text, { title: action.title, @@ -372,7 +349,7 @@ export class ApplicationService { if (currentAppId === undefined) { return; } - const action = getLeaveAction(this.appLeaveHandlers.get(currentAppId)); + const action = getLeaveAction(this.appInternalStates.get(currentAppId)?.leaveHandler); if (isConfirmAction(action)) { event.preventDefault(); // some browsers accept a string return value being the message displayed @@ -383,13 +360,14 @@ export class ApplicationService { public stop() { this.stop$.next(); this.currentAppId$.complete(); + this.currentActionMenu$.complete(); this.statusUpdaters$.complete(); this.subscriptions.forEach((sub) => sub.unsubscribe()); window.removeEventListener('beforeunload', this.onBeforeUnload); } } -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 b0419d276dfa1..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,11 @@ 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'; const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); @@ -54,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(); }); @@ -147,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); @@ -309,4 +259,189 @@ describe('ApplicationService', () => { expect(history.entries[1].pathname).toEqual('/app/app1'); }); }); + + describe('registering action menus', () => { + const getValue = (obs: Observable): Promise => { + return obs.pipe(take(1)).toPromise(); + }; + + const mounter1: MountPoint = () => () => undefined; + const mounter2: MountPoint = () => () => undefined; + + it('updates the observable value when an application is mounted', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + expect(await getValue(currentActionMenu$)).toBeUndefined(); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + }); + + it('updates the observable value when switching application', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + return () => undefined; + }, + }); + register(Symbol(), { + id: 'app2', + title: 'App2', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter2); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + await navigateToApp('app2'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter2); + }); + + it('updates the observable value to undefined when switching to an application without action menu', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + return () => undefined; + }, + }); + register(Symbol(), { + id: 'app2', + title: 'App2', + mount: async ({}: AppMountParameters) => { + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + await navigateToApp('app2'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBeUndefined(); + }); + + it('allow applications to call `setHeaderActionMenu` multiple times', async () => { + const { register } = service.setup(setupDeps); + + let resolveMount: () => void; + const promise = new Promise((resolve) => { + resolveMount = resolve; + }); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + promise.then(() => { + setHeaderActionMenu(mounter2); + }); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + resolveMount(); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter2); + }); + + it('allow applications to unset the current menu', async () => { + const { register } = service.setup(setupDeps); + + let resolveMount: () => void; + const promise = new Promise((resolve) => { + resolveMount = resolve; + }); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + setHeaderActionMenu(mounter1); + promise.then(() => { + setHeaderActionMenu(undefined); + }); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + resolveMount(); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBeUndefined(); + }); + }); }); diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index f992e121437a9..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; @@ -59,6 +58,7 @@ describe('AppRouter', () => { mounters={mockMountersToMounters()} appStatuses$={mountersToAppStatus$()} setAppLeaveHandler={noop} + setAppActionMenu={noop} setIsMounting={noop} /> ); @@ -66,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
', @@ -80,7 +78,6 @@ describe('AppRouter', () => { appRoute: '/chromeless-b/path', }), createAppMounter({ appId: 'disabledApp', html: '
Disabled app
' }), - createLegacyAppMounter('disabledLegacyApp', jest.fn()), createAppMounter({ appId: 'scopedApp', extraMountHook: ({ history }) => { @@ -98,7 +95,7 @@ describe('AppRouter', () => { html: '
App 6
', appRoute: '/app/my-app/app6', }), - ] as Array>); + ] as MockedMounterTuple[]); globalHistory = createMemoryHistory(); update = createMountersRenderer(); }); @@ -383,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'); @@ -414,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 0fe97431b1569..df83b6e932aad 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -21,6 +21,7 @@ import { Observable } from 'rxjs'; import { History } from 'history'; import { RecursiveReadonly } from '@kbn/utility-types'; +import { MountPoint } from '../types'; import { Capabilities } from './capabilities'; import { ChromeStart } from '../chrome'; import { IContextProvider } from '../context'; @@ -35,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 */ @@ -135,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}. @@ -222,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 @@ -254,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. * @@ -299,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. * @@ -495,6 +460,37 @@ export interface AppMountParameters { * ``` */ onAppLeave: (handler: AppLeaveHandler) => void; + + /** + * A function that can be used to set the mount point used to populate the application action container + * in the chrome header. + * + * Calling the handler multiple time will erase the current content of the action menu with the mount from the latest call. + * Calling the handler with `undefined` will unmount the current mount point. + * Calling the handler after the application has been unmounted will have no effect. + * + * @example + * + * ```ts + * // application.tsx + * import React from 'react'; + * import ReactDOM from 'react-dom'; + * import { BrowserRouter, Route } from 'react-router-dom'; + * + * import { CoreStart, AppMountParameters } from 'src/core/public'; + * import { MyPluginDepsStart } from './plugin'; + * + * export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => { + * const { renderApp } = await import('./application'); + * const { renderActionMenu } = await import('./action_menu'); + * setHeaderActionMenu((element) => { + * return renderActionMenu(element); + * }) + * return renderApp({ element, history }); + * } + * ``` + */ + setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; } /** @@ -575,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 { @@ -670,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}. @@ -699,7 +672,7 @@ export interface InternalApplicationSetup extends Pick>; + applications$: Observable>; /** * Navigate to a given app @@ -821,14 +791,16 @@ export interface InternalApplicationStart extends Omit | undefined; -} + currentActionMenu$: Observable; -/** @internal */ -type SelectivePartial = Partial> & - Required>> extends infer U - ? { [P in keyof U]: U[P] } - : never; + /** + * The global history instance, exposed only to Core. + * @internal + */ + history: History; +} diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx index a94313dd53abb..f6cde54e6f502 100644 --- a/src/core/public/application/ui/app_container.test.tsx +++ b/src/core/public/application/ui/app_container.test.tsx @@ -29,6 +29,7 @@ import { ScopedHistory } from '../scoped_history'; describe('AppContainer', () => { const appId = 'someApp'; const setAppLeaveHandler = jest.fn(); + const setAppActionMenu = jest.fn(); const setIsMounting = jest.fn(); beforeEach(() => { @@ -54,7 +55,6 @@ describe('AppContainer', () => { appBasePath: '/base-path', appRoute: '/some-route', unmountBeforeMounting: false, - legacy: false, exactRoute: false, mount: async ({ element }: AppMountParameters) => { await promise; @@ -76,6 +76,7 @@ describe('AppContainer', () => { appStatus={AppStatus.inaccessible} mounter={mounter} setAppLeaveHandler={setAppLeaveHandler} + setAppActionMenu={setAppActionMenu} setIsMounting={setIsMounting} createScopedHistory={(appPath: string) => // Create a history using the appPath as the current location @@ -116,6 +117,7 @@ describe('AppContainer', () => { appStatus={AppStatus.accessible} mounter={mounter} setAppLeaveHandler={setAppLeaveHandler} + setAppActionMenu={setAppActionMenu} setIsMounting={setIsMounting} createScopedHistory={(appPath: string) => // Create a history using the appPath as the current location @@ -143,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; @@ -158,6 +159,7 @@ describe('AppContainer', () => { appStatus={AppStatus.accessible} mounter={mounter} setAppLeaveHandler={setAppLeaveHandler} + setAppActionMenu={setAppActionMenu} setIsMounting={setIsMounting} createScopedHistory={(appPath: string) => // Create a history using the appPath as the current location diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 332c31c64b6ba..f668cf851da55 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -25,8 +25,9 @@ import React, { useState, MutableRefObject, } from 'react'; - import { EuiLoadingSpinner } from '@elastic/eui'; + +import type { MountPoint } from '../../types'; import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; import { AppNotFound } from './app_not_found_screen'; import { ScopedHistory } from '../scoped_history'; @@ -39,6 +40,7 @@ interface Props { mounter?: Mounter; appStatus: AppStatus; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; + setAppActionMenu: (appId: string, mount: MountPoint | undefined) => void; createScopedHistory: (appUrl: string) => ScopedHistory; setIsMounting: (isMounting: boolean) => void; } @@ -48,6 +50,7 @@ export const AppContainer: FunctionComponent = ({ appId, appPath, setAppLeaveHandler, + setAppActionMenu, createScopedHistory, appStatus, setIsMounting, @@ -84,6 +87,7 @@ export const AppContainer: FunctionComponent = ({ history: createScopedHistory(appPath), element: elementRef.current!, onAppLeave: (handler) => setAppLeaveHandler(appId, handler), + setHeaderActionMenu: (menuMount) => setAppActionMenu(appId, menuMount), })) || null; } catch (e) { // TODO: add error UI @@ -98,7 +102,16 @@ export const AppContainer: FunctionComponent = ({ mount(); return unmount; - }, [appId, appStatus, mounter, createScopedHistory, setAppLeaveHandler, appPath, setIsMounting]); + }, [ + appId, + appStatus, + mounter, + createScopedHistory, + setAppLeaveHandler, + setAppActionMenu, + appPath, + setIsMounting, + ]); return ( diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index f1f22237c32db..42bc9a53aee2d 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -23,6 +23,7 @@ import { History } from 'history'; import { Observable } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; +import type { MountPoint } from '../../types'; import { AppLeaveHandler, AppStatus, Mounter } from '../types'; import { AppContainer } from './app_container'; import { ScopedHistory } from '../scoped_history'; @@ -32,6 +33,7 @@ interface Props { history: History; appStatuses$: Observable>; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; + setAppActionMenu: (appId: string, mount: MountPoint | undefined) => void; setIsMounting: (isMounting: boolean) => void; } @@ -43,6 +45,7 @@ export const AppRouter: FunctionComponent = ({ history, mounters, setAppLeaveHandler, + setAppActionMenu, appStatuses$, setIsMounting, }) => { @@ -55,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/recently_accessed/persisted_log.test.ts b/src/core/public/chrome/recently_accessed/persisted_log.test.ts index 4229efdf7ca9d..345ad8f3a1f5a 100644 --- a/src/core/public/chrome/recently_accessed/persisted_log.test.ts +++ b/src/core/public/chrome/recently_accessed/persisted_log.test.ts @@ -28,12 +28,6 @@ const createMockStorage = () => ({ length: 0, }); -jest.mock('ui/chrome', () => { - return { - getBasePath: () => `/some/base/path`, - }; -}); - const historyName = 'testHistory'; const historyLimit = 10; const payload = [ 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 fe959e570ab98..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", }, ], @@ -650,6 +640,8 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` data-test-subj="collapsibleNavGroup-recentlyViewed" id="mockId" initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} paddingSize="none" > @@ -901,6 +893,8 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` data-test-subj="collapsibleNavGroup-kibana" id="mockId" initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} paddingSize="none" > @@ -1188,6 +1182,8 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} paddingSize="none" > @@ -1436,6 +1432,8 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` data-test-subj="collapsibleNavGroup-securitySolution" id="mockId" initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} paddingSize="none" > @@ -1636,6 +1634,8 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` data-test-subj="collapsibleNavGroup-management" id="mockId" initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} paddingSize="none" > @@ -2106,7 +2106,6 @@ exports[`CollapsibleNav renders the default nav 1`] = ` id="collapsibe-nav" isLocked={false} isOpen={false} - legacyMode={false} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2341,7 +2340,6 @@ exports[`CollapsibleNav renders the default nav 2`] = ` id="collapsibe-nav" isLocked={false} isOpen={true} - legacyMode={false} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2624,6 +2622,8 @@ exports[`CollapsibleNav renders the default nav 2`] = ` data-test-subj="collapsibleNavGroup-recentlyViewed" id="mockId" initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} paddingSize="none" > @@ -3026,7 +3026,6 @@ exports[`CollapsibleNav renders the default nav 3`] = ` id="collapsibe-nav" isLocked={true} isOpen={true} - legacyMode={false} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -3305,6 +3304,8 @@ exports[`CollapsibleNav renders the default nav 3`] = ` data-test-subj="collapsibleNavGroup-recentlyViewed" id="mockId" initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} paddingSize="none" > 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 f101c30a1241b..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 @@ -29,6 +29,15 @@ exports[`Header renders 1`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentActionMenu$": BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, "currentAppId$": Observable { "_isScalar": false, "source": Subject { @@ -106,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, @@ -353,7 +362,6 @@ exports[`Header renders 1`] = ` } kibanaDocLink="/docs" kibanaVersion="1.0.0" - legacyMode={false} loadingCount$={ BehaviorSubject { "_isScalar": false, @@ -431,8 +439,8 @@ exports[`Header renders 1`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -641,6 +649,15 @@ exports[`Header renders 2`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentActionMenu$": BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, "currentAppId$": Observable { "_isScalar": false, "source": Subject { @@ -832,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, @@ -1871,7 +1888,6 @@ exports[`Header renders 2`] = ` } kibanaDocLink="/docs" kibanaVersion="1.0.0" - legacyMode={false} loadingCount$={ BehaviorSubject { "_isScalar": false, @@ -2025,8 +2041,8 @@ exports[`Header renders 2`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -2397,8 +2413,8 @@ exports[`Header renders 2`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -4569,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, @@ -4622,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", }, ], @@ -4854,6 +4869,15 @@ exports[`Header renders 3`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentActionMenu$": BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, "currentAppId$": Observable { "_isScalar": false, "source": Subject { @@ -5045,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, @@ -6084,7 +6108,6 @@ exports[`Header renders 3`] = ` } kibanaDocLink="/docs" kibanaVersion="1.0.0" - legacyMode={false} loadingCount$={ BehaviorSubject { "_isScalar": false, @@ -6238,8 +6261,8 @@ exports[`Header renders 3`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -6610,8 +6633,8 @@ exports[`Header renders 3`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -8782,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, @@ -8835,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", }, ], @@ -9056,7 +9078,7 @@ exports[`Header renders 3`] = ` Array [ Object { "data-test-subj": "collapsibleNavCustomNavLink", - "href": undefined, + "href": "", "icon": undefined, "iconType": undefined, "isActive": false, @@ -9080,6 +9102,7 @@ exports[`Header renders 3`] = ` @@ -9451,6 +9476,7 @@ exports[`Header renders 3`] = ` @@ -14098,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 2f7f6fae94436..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'; @@ -166,6 +165,7 @@ function createAppMountParametersMock(appBasePath = '') { element: document.createElement('div'), history, onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), }; return params; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6f25f46c76fb9..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?: { @@ -165,6 +161,7 @@ export interface AppMountParameters { element: HTMLElement; history: ScopedHistory; onAppLeave: (handler: AppLeaveHandler) => void; + setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; } // @public @@ -185,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; @@ -226,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; } @@ -289,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; } @@ -323,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 { @@ -880,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; @@ -1039,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; @@ -1188,8 +1119,10 @@ export interface SavedObjectsFindOptions { // (undocumented) defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; + // Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts + // // (undocumented) - filter?: string; + filter?: string | KueryNode; // (undocumented) hasReference?: { type: string; @@ -1542,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/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/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/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 4d9bcdda3c8ae..60e8aa0afdda4 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -83,7 +83,19 @@ const mockMappings = { describe('Filter Utils', () => { describe('#validateConvertFilterToKueryNode', () => { - test('Validate a simple filter', () => { + test('Empty string filters are ignored', () => { + expect(validateConvertFilterToKueryNode(['foo'], '', mockMappings)).toBeUndefined(); + }); + test('Validate a simple KQL KueryNode filter', () => { + expect( + validateConvertFilterToKueryNode( + ['foo'], + esKuery.nodeTypes.function.buildNode('is', `foo.attributes.title`, 'best', true), + mockMappings + ) + ).toEqual(esKuery.fromKueryExpression('foo.title: "best"')); + }); + test('Validate a simple KQL expression filter', () => { expect( validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockMappings) ).toEqual(esKuery.fromKueryExpression('foo.title: "best"')); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index 5fbe62a074b29..d19f06d74e419 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -28,11 +28,12 @@ const astFunctionType = ['is', 'range', 'nested']; export const validateConvertFilterToKueryNode = ( allowedTypes: string[], - filter: string, + filter: string | KueryNode, indexMapping: IndexMapping ): KueryNode | undefined => { - if (filter && filter.length > 0 && indexMapping) { - const filterKueryNode = esKuery.fromKueryExpression(filter); + if (filter && indexMapping) { + const filterKueryNode = + typeof filter === 'string' ? esKuery.fromKueryExpression(filter) : filter; const validationFilterKuery = validateFilterKueryNode({ astFilter: filterKueryNode, diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 39433981dfd59..b1d6028465713 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -25,6 +25,8 @@ import { encodeHitVersion } from '../../version'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { DocumentMigrator } from '../../migrations/core/document_migrator'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { nodeTypes } from '../../../../../plugins/data/common/es_query'; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -2529,7 +2531,7 @@ describe('SavedObjectsRepository', () => { expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, relevantOpts); }); - it(`accepts KQL filter and passes kueryNode to getSearchDsl`, async () => { + it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => { const findOpts = { namespace, search: 'foo*', @@ -2570,6 +2572,47 @@ describe('SavedObjectsRepository', () => { `); }); + it(`accepts KQL KueryNode filter and passes KueryNode to getSearchDsl`, async () => { + const findOpts = { + namespace, + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + indexPattern: undefined, + filter: nodeTypes.function.buildNode('is', `dashboard.attributes.otherField`, '*'), + }; + + await findSuccess(findOpts, namespace); + const { kueryNode } = getSearchDslNS.getSearchDsl.mock.calls[0][2]; + expect(kueryNode).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "dashboard.otherField", + }, + Object { + "type": "wildcard", + "value": "@kuery-wildcard@", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + it(`supports multiple types`, async () => { const types = ['config', 'index-pattern']; await findSuccess({ type: types }); diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index edbdbe4d16784..000153cd542fa 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -39,6 +39,9 @@ import { SavedObjectUnsanitizedDoc } from './serialization'; import { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; import { SavedObject } from '../../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KueryNode } from '../../../plugins/data/common'; + export { SavedObjectAttributes, SavedObjectAttribute, @@ -89,7 +92,7 @@ export interface SavedObjectsFindOptions { rootSearchFields?: string[]; hasReference?: { type: string; id: string }; defaultSearchOperator?: 'AND' | 'OR'; - filter?: string; + filter?: string | KueryNode; namespaces?: string[]; /** An optional ES preference value to be used for the query **/ preference?: string; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 49c97d837579d..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'; @@ -2320,8 +2323,10 @@ export interface SavedObjectsFindOptions { // (undocumented) defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; + // Warning: (ae-forgotten-export) The symbol "KueryNode" needs to be exported by the entry point index.d.ts + // // (undocumented) - filter?: string; + filter?: string | KueryNode; // (undocumented) hasReference?: { type: string; 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 b4cfc3c1efe8b..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'; @@ -73,9 +73,9 @@ export function getServices() { httpServerMock.createKibanaRequest() ); - const uiSettings = kbnServer.server.uiSettingsServiceFactory({ - savedObjectsClient, - }); + const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient( + savedObjectsClient + ); services = { kbnServer, 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/core/server/utils/streams/concat_stream.ts b/src/core/server/utils/streams/concat_stream.ts new file mode 100644 index 0000000000000..03450cb51b832 --- /dev/null +++ b/src/core/server/utils/streams/concat_stream.ts @@ -0,0 +1,46 @@ +/* + * 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 { createReduceStream } from './reduce_stream'; + +/** + * 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]" + * + * + * @param {any} initial The initial value that subsequent + * items will concat with + * @return {Transform} + */ +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/core/server/utils/streams/concat_stream_providers.ts b/src/core/server/utils/streams/concat_stream_providers.ts new file mode 100644 index 0000000000000..bb836e3d73787 --- /dev/null +++ b/src/core/server/utils/streams/concat_stream_providers.ts @@ -0,0 +1,64 @@ +/* + * 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, 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. + * + * @param {Array<() => ReadableStream>} sourceProviders + * @param {PassThroughOptions} options options passed to the PassThrough constructor + * @return {WritableStream} combined stream + */ +export function concatStreamProviders( + sourceProviders: Array<() => Readable>, + 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/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/core/server/utils/streams/index.ts b/src/core/server/utils/streams/index.ts new file mode 100644 index 0000000000000..447d1ed5b1c53 --- /dev/null +++ b/src/core/server/utils/streams/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { 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/core/server/utils/streams/intersperse_stream.ts b/src/core/server/utils/streams/intersperse_stream.ts new file mode 100644 index 0000000000000..272507221caff --- /dev/null +++ b/src/core/server/utils/streams/intersperse_stream.ts @@ -0,0 +1,64 @@ +/* + * 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" + * + * @param {String|Buffer} intersperseChunk + * @return {Transform} + */ +export function createIntersperseStream(intersperseChunk: string | Buffer) { + let first = true; + + return new Transform({ + writableObjectMode: true, + readableObjectMode: true, + transform(chunk, enc, callback) { + try { + if (first) { + first = false; + } else { + this.push(intersperseChunk); + } + + this.push(chunk); + callback(); + } catch (err) { + callback(err); + } + }, + }); +} diff --git a/src/core/server/utils/streams/list_stream.test.ts b/src/core/server/utils/streams/list_stream.test.ts new file mode 100644 index 0000000000000..2a20c929db6b9 --- /dev/null +++ b/src/core/server/utils/streams/list_stream.test.ts @@ -0,0 +1,44 @@ +/* + * 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 } from './index'; + +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); + + 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/core/server/utils/streams/list_stream.ts b/src/core/server/utils/streams/list_stream.ts new file mode 100644 index 0000000000000..e62f6d3fa930b --- /dev/null +++ b/src/core/server/utils/streams/list_stream.ts @@ -0,0 +1,44 @@ +/* + * 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'; + +/** + * Create a Readable stream that provides the items + * from a list as objects to subscribers + * + * @param {Array} items - the list of items to provide + * @return {Readable} + */ +export function createListStream(items: T | T[] = []) { + const queue = Array.isArray(items) ? [...items] : [items]; + + return new Readable({ + objectMode: true, + read(size) { + queue.splice(0, size).forEach((item) => { + this.push(item); + }); + + if (!queue.length) { + this.push(null); + } + }, + }); +} 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/core/server/utils/streams/map_stream.ts b/src/core/server/utils/streams/map_stream.ts new file mode 100644 index 0000000000000..aad53cc526626 --- /dev/null +++ b/src/core/server/utils/streams/map_stream.ts @@ -0,0 +1,36 @@ +/* + * 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 createMapStream(fn: (value: T, i: number) => void) { + let i = 0; + + return new Transform({ + objectMode: true, + async transform(value, enc, done) { + try { + this.push(await fn(value, i++)); + done(); + } catch (err) { + done(err); + } + }, + }); +} 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/core/server/utils/streams/reduce_stream.ts b/src/core/server/utils/streams/reduce_stream.ts new file mode 100644 index 0000000000000..9129df096ad13 --- /dev/null +++ b/src/core/server/utils/streams/reduce_stream.ts @@ -0,0 +1,83 @@ +/* + * 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. + * + * @param {Function} + * @param {any} initial Initial value for the stream, if undefined + * then the first chunk provided is used as the + * initial value. + * @return {Transform} + */ +export function createReduceStream( + reducer: (value: any, chunk: T, enc: string) => T, + initial?: T +) { + 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/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/core/server/utils/streams/replace_stream.ts b/src/core/server/utils/streams/replace_stream.ts new file mode 100644 index 0000000000000..05391bb3341c2 --- /dev/null +++ b/src/core/server/utils/streams/replace_stream.ts @@ -0,0 +1,86 @@ +/* + * 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 | Buffer) { + 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, enc, 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); + } + + // @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/core/server/utils/streams/split_stream.ts b/src/core/server/utils/streams/split_stream.ts new file mode 100644 index 0000000000000..ae820f60abbf6 --- /dev/null +++ b/src/core/server/utils/streams/split_stream.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 { 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 + * + * @param {String} splitChunk + * @return {Transform} + */ +export function createSplitStream(splitChunk: string | Uint8Array) { + let unsplitBuffer = Buffer.alloc(0); + + return new Transform({ + writableObjectMode: false, + readableObjectMode: true, + transform(chunk, enc, 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(); + } catch (err) { + callback(err); + } + }, + + flush(callback) { + try { + this.push(unsplitBuffer.toString('utf8')); + + 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/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/build/tasks/create_archives_task.ts b/src/dev/build/tasks/create_archives_task.ts index 0083881e9f748..a05e383394ecf 100644 --- a/src/dev/build/tasks/create_archives_task.ts +++ b/src/dev/build/tasks/create_archives_task.ts @@ -92,8 +92,8 @@ export const CreateArchives: Task = { }); metrics.push({ - group: `${build.isOss() ? 'oss ' : ''}distributable file count`, - id: 'total', + group: 'distributable file count', + id: build.isOss() ? 'oss' : 'default', value: fileCount, }); } diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 74e1ec5e2b4ed..486c8563c5456 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -59,7 +59,6 @@ export default { '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '^src/plugins/(.*)': '/src/plugins/$1', '^plugins/([^/.]*)(.*)': '/src/legacy/core_plugins/$1/public$2', - '^ui/(.*)': '/src/legacy/ui/public/$1', '^uiExports/(.*)': '/src/dev/jest/mocks/file_mock.js', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', diff --git a/src/dev/jest/setup/mocks.js b/src/dev/jest/setup/mocks.js index 6e7160e858cd7..cea28d8abdbd1 100644 --- a/src/dev/jest/setup/mocks.js +++ b/src/dev/jest/setup/mocks.js @@ -34,10 +34,6 @@ * The mocks that are enabled that way live inside the `__mocks__` folders beside their implementation files. */ -jest.mock('ui/metadata'); -jest.mock('ui/documentation_links/documentation_links'); -jest.mock('ui/chrome'); - jest.mock('moment-timezone', () => { // We always want to mock the timezone moment-timezone guesses, since otherwise // test results might be depending on which time zone you are running them. 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/dev/typescript/build_refs.ts b/src/dev/typescript/build_refs.ts new file mode 100644 index 0000000000000..cbb596c185f8b --- /dev/null +++ b/src/dev/typescript/build_refs.ts @@ -0,0 +1,37 @@ +/* + * 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 execa from 'execa'; +import { run, ToolingLog } from '@kbn/dev-utils'; + +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); + } +} + +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/fixtures/stubbed_logstash_index_pattern.js b/src/fixtures/stubbed_logstash_index_pattern.js index 5bb926799fcf6..5735b01eb3db4 100644 --- a/src/fixtures/stubbed_logstash_index_pattern.js +++ b/src/fixtures/stubbed_logstash_index_pattern.js @@ -21,7 +21,12 @@ import StubIndexPattern from 'test_utils/stub_index_pattern'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { getKbnFieldType } from '../plugins/data/common'; -import { npSetup } from '../legacy/ui/public/new_platform/new_platform.karma_mock'; +import { uiSettingsServiceMock } from '../core/public/ui_settings/ui_settings_service.mock'; + +const uiSettingSetupMock = uiSettingsServiceMock.createSetupContract(); +uiSettingSetupMock.get.mockImplementation((item, defaultValue) => { + return defaultValue; +}); export default function stubbedLogstashIndexPatternService() { const mockLogstashFields = stubbedLogstashFields(); @@ -41,13 +46,9 @@ export default function stubbedLogstashIndexPatternService() { }; }); - const indexPattern = new StubIndexPattern( - 'logstash-*', - (cfg) => cfg, - 'time', - fields, - npSetup.core - ); + const indexPattern = new StubIndexPattern('logstash-*', (cfg) => cfg, 'time', fields, { + uiSettings: uiSettingSetupMock, + }); indexPattern.id = 'logstash-*'; indexPattern.isTimeNanosBased = () => false; diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js deleted file mode 100644 index 722d75d00f78f..0000000000000 --- a/src/legacy/core_plugins/kibana/index.js +++ /dev/null @@ -1,39 +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 { getUiSettingDefaults } from './server/ui_setting_defaults'; - -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(); - }, - - uiExports: { - uiSettingDefaults: getUiSettingDefaults(), - }, - }); -} 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/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js deleted file mode 100644 index 7de5fb581643a..0000000000000 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ /dev/null @@ -1,23 +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 getUiSettingDefaults() { - // wrapped in provider so that a new instance is given to each app/test - return {}; -} 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/create_transform.js b/src/legacy/deprecation/create_transform.js deleted file mode 100644 index 72e8e153ed819..0000000000000 --- a/src/legacy/deprecation/create_transform.js +++ /dev/null @@ -1,33 +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 { deepCloneWithBuffers as clone } from '../utils'; -import { forEach, noop } from 'lodash'; - -export function createTransform(deprecations) { - return (settings, log = noop) => { - const result = clone(settings); - - forEach(deprecations, (deprecation) => { - deprecation(result, log); - }); - - return result; - }; -} 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/deprecation/deprecations/unused.js b/src/legacy/deprecation/deprecations/unused.js deleted file mode 100644 index 4291063dc482b..0000000000000 --- a/src/legacy/deprecation/deprecations/unused.js +++ /dev/null @@ -1,33 +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 { get, isUndefined, noop } from 'lodash'; -import { unset } from '../../utils'; - -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`); - }; -} diff --git a/src/legacy/deprecation/get_transform.js b/src/legacy/deprecation/get_transform.js deleted file mode 100644 index bf286901af62c..0000000000000 --- a/src/legacy/deprecation/get_transform.js +++ /dev/null @@ -1,30 +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 { noop } from 'lodash'; - -import { createTransform } from './create_transform'; -import { rename, unused } from './deprecations'; - -export async function getTransform(spec) { - const deprecationsProvider = spec.getDeprecationsProvider() || noop; - if (!deprecationsProvider) return; - const transforms = (await deprecationsProvider({ rename, unused })) || []; - return createTransform(transforms); -} diff --git a/src/legacy/deprecation/index.js b/src/legacy/deprecation/index.js deleted file mode 100644 index 787563e7353ce..0000000000000 --- a/src/legacy/deprecation/index.js +++ /dev/null @@ -1,24 +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 { rename, unused } from './deprecations'; - -export { createTransform } from './create_transform'; -export { getTransform } from './get_transform'; -export const Deprecations = { rename, unused }; 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/capabilities/capabilities_mixin.test.ts b/src/legacy/server/capabilities/capabilities_mixin.test.ts deleted file mode 100644 index 3422d6a8cbb34..0000000000000 --- a/src/legacy/server/capabilities/capabilities_mixin.test.ts +++ /dev/null @@ -1,65 +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 { Server } from 'hapi'; -import KbnServer from '../kbn_server'; - -import { capabilitiesMixin } from './capabilities_mixin'; - -describe('capabilitiesMixin', () => { - let registerMock: jest.Mock; - - const getKbnServer = (pluginSpecs: any[] = []) => { - return ({ - afterPluginsInit: (callback: () => void) => callback(), - pluginSpecs, - newPlatform: { - setup: { - core: { - capabilities: { - registerProvider: registerMock, - }, - }, - }, - }, - } as unknown) as KbnServer; - }; - - let server: Server; - beforeEach(() => { - server = new Server(); - server.getUiNavLinks = () => []; - registerMock = jest.fn(); - }); - - it('calls capabilities#registerCapabilitiesProvider for each legacy plugin specs', async () => { - const getPluginSpec = (provider: () => any) => ({ - getUiCapabilitiesProvider: () => provider, - }); - - const capaA = { catalogue: { A: true } }; - const capaB = { catalogue: { B: true } }; - const kbnServer = getKbnServer([getPluginSpec(() => capaA), getPluginSpec(() => capaB)]); - await capabilitiesMixin(kbnServer, server); - - expect(registerMock).toHaveBeenCalledTimes(2); - expect(registerMock.mock.calls[0][0]()).toEqual(capaA); - expect(registerMock.mock.calls[1][0]()).toEqual(capaB); - }); -}); diff --git a/src/legacy/server/capabilities/capabilities_mixin.ts b/src/legacy/server/capabilities/capabilities_mixin.ts deleted file mode 100644 index 1f8c869f17f66..0000000000000 --- a/src/legacy/server/capabilities/capabilities_mixin.ts +++ /dev/null @@ -1,42 +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 { Server } from 'hapi'; -import KbnServer from '../kbn_server'; - -export async function capabilitiesMixin(kbnServer: KbnServer, server: Server) { - const registerLegacyCapabilities = async () => { - const capabilitiesList = await Promise.all( - kbnServer.pluginSpecs - .map((spec) => spec.getUiCapabilitiesProvider()) - .filter((provider) => !!provider) - .map((provider) => provider(server)) - ); - - capabilitiesList.forEach((capabilities) => { - kbnServer.newPlatform.setup.core.capabilities.registerProvider(() => capabilities); - }); - }; - - // Some plugin capabilities are derived from data provided by other plugins, - // so we need to wait until after all plugins have been init'd to fetch uiCapabilities. - kbnServer.afterPluginsInit(async () => { - await registerLegacyCapabilities(); - }); -} diff --git a/src/legacy/server/capabilities/index.ts b/src/legacy/server/capabilities/index.ts deleted file mode 100644 index 8c5dea1226f2b..0000000000000 --- a/src/legacy/server/capabilities/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 { capabilitiesMixin } from './capabilities_mixin'; 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 9bb091383ab13..69fb63fbbd87f 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -45,16 +45,11 @@ import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/serve import { UiPlugins } from '../../core/server/plugins'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; -import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory'; 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 { @@ -67,18 +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; - uiSettingsServiceFactory: (options?: UiSettingsServiceFactoryOptions) => IUiSettingsClient; logWithMetadata: (tags: string[], message: string, meta: Record) => void; newPlatform: KbnServer['newPlatform']; } @@ -88,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 1084521235ea0..4692262d99bb5 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -34,8 +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 { capabilitiesMixin } from './capabilities'; -import { serverExtensionsMixin } from './server_extensions'; import { uiMixin } from '../ui'; import { i18nMixin } from './i18n'; @@ -92,8 +90,6 @@ export default class KbnServer { coreMixin, - // adds methods for extending this.server - serverExtensionsMixin, loggingMixin, warningsMixin, statusMixin, @@ -115,9 +111,6 @@ export default class KbnServer { // setup saved object routes savedObjectsMixin, - // setup capabilities routes - capabilitiesMixin, - // setup routes that serve the @kbn/optimizer output optimizeMixin, 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/index.js b/src/legacy/server/server_extensions/index.js deleted file mode 100644 index e17bd488897f7..0000000000000 --- a/src/legacy/server/server_extensions/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 { serverExtensionsMixin } from './server_extensions_mixin'; 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/public/.eslintrc b/src/legacy/ui/public/.eslintrc deleted file mode 100644 index cc44af915ba25..0000000000000 --- a/src/legacy/ui/public/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -rules: - no-console: 2 - 'import/no-default-export': error diff --git a/src/legacy/ui/public/__mocks__/metadata.ts b/src/legacy/ui/public/__mocks__/metadata.ts deleted file mode 100644 index b7f944de27463..0000000000000 --- a/src/legacy/ui/public/__mocks__/metadata.ts +++ /dev/null @@ -1,25 +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 { metadata as metadataImpl } from '../metadata'; - -export const metadata: typeof metadataImpl = { - branch: 'jest-metadata-mock-branch', - version: '42.23.26', -}; diff --git a/src/legacy/ui/public/__tests__/events.js b/src/legacy/ui/public/__tests__/events.js deleted file mode 100644 index c225c2a8ac1c0..0000000000000 --- a/src/legacy/ui/public/__tests__/events.js +++ /dev/null @@ -1,244 +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 _ from 'lodash'; -import sinon from 'sinon'; -import ngMock from 'ng_mock'; -import { EventsProvider } from '../events'; -import expect from '@kbn/expect'; -import '../private'; -import { createDefer } from 'ui/promises'; -import { createLegacyClass } from '../utils/legacy_class'; - -describe('Events', function () { - require('test_utils/no_digest_promises').activateForSuite(); - - let Events; - let Promise; - let eventsInstance; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function ($injector, Private) { - Promise = $injector.get('Promise'); - Events = Private(EventsProvider); - eventsInstance = new Events(); - }) - ); - - it('should handle on events', function () { - const obj = new Events(); - const prom = obj.on('test', function (message) { - expect(message).to.equal('Hello World'); - }); - - obj.emit('test', 'Hello World'); - - return prom; - }); - - it('should work with inherited objects', function () { - createLegacyClass(MyEventedObject).inherits(Events); - function MyEventedObject() { - MyEventedObject.Super.call(this); - } - const obj = new MyEventedObject(); - - const prom = obj.on('test', function (message) { - expect(message).to.equal('Hello World'); - }); - - obj.emit('test', 'Hello World'); - - return prom; - }); - - it('should clear events when off is called', function () { - const obj = new Events(); - obj.on('test', _.noop); - expect(obj._listeners).to.have.property('test'); - expect(obj._listeners.test).to.have.length(1); - obj.off(); - expect(obj._listeners).to.not.have.property('test'); - }); - - it('should clear a specific handler when off is called for an event', function () { - const obj = new Events(); - const handler1 = sinon.stub(); - const handler2 = sinon.stub(); - obj.on('test', handler1); - obj.on('test', handler2); - expect(obj._listeners).to.have.property('test'); - obj.off('test', handler1); - - return obj.emit('test', 'Hello World').then(function () { - sinon.assert.calledOnce(handler2); - sinon.assert.notCalled(handler1); - }); - }); - - it('should clear a all handlers when off is called for an event', function () { - const obj = new Events(); - const handler1 = sinon.stub(); - obj.on('test', handler1); - expect(obj._listeners).to.have.property('test'); - obj.off('test'); - expect(obj._listeners).to.not.have.property('test'); - - return obj.emit('test', 'Hello World').then(function () { - sinon.assert.notCalled(handler1); - }); - }); - - it('should handle multiple identical emits in the same tick', function () { - const obj = new Events(); - const handler1 = sinon.stub(); - - obj.on('test', handler1); - const emits = [obj.emit('test', 'one'), obj.emit('test', 'two'), obj.emit('test', 'three')]; - - return Promise.all(emits).then(function () { - expect(handler1.callCount).to.be(emits.length); - expect(handler1.getCall(0).calledWith('one')).to.be(true); - expect(handler1.getCall(1).calledWith('two')).to.be(true); - expect(handler1.getCall(2).calledWith('three')).to.be(true); - }); - }); - - it('should handle emits from the handler', function () { - const obj = new Events(); - const secondEmit = createDefer(Promise); - - const handler1 = sinon.spy(function () { - if (handler1.calledTwice) { - return; - } - obj.emit('test').then(_.bindKey(secondEmit, 'resolve')); - }); - - obj.on('test', handler1); - - return Promise.all([obj.emit('test'), secondEmit.promise]).then(function () { - expect(handler1.callCount).to.be(2); - }); - }); - - it('should only emit to handlers registered before emit is called', function () { - const obj = new Events(); - const handler1 = sinon.stub(); - const handler2 = sinon.stub(); - - obj.on('test', handler1); - const emits = [obj.emit('test', 'one'), obj.emit('test', 'two'), obj.emit('test', 'three')]; - - return Promise.all(emits).then(function () { - expect(handler1.callCount).to.be(emits.length); - - obj.on('test', handler2); - - const emits2 = [obj.emit('test', 'four'), obj.emit('test', 'five'), obj.emit('test', 'six')]; - - return Promise.all(emits2).then(function () { - expect(handler1.callCount).to.be(emits.length + emits2.length); - expect(handler2.callCount).to.be(emits2.length); - }); - }); - }); - - it('should pass multiple arguments from the emitter', function () { - const obj = new Events(); - const handler = sinon.stub(); - const payload = ['one', { hello: 'tests' }, null]; - - obj.on('test', handler); - - return obj.emit('test', payload[0], payload[1], payload[2]).then(function () { - expect(handler.callCount).to.be(1); - expect(handler.calledWithExactly(payload[0], payload[1], payload[2])).to.be(true); - }); - }); - - it('should preserve the scope of the handler', function () { - const obj = new Events(); - const expected = 'some value'; - let testValue; - - function handler() { - testValue = this.getVal(); - } - handler.getVal = _.constant(expected); - - obj.on('test', handler); - return obj.emit('test').then(function () { - expect(testValue).to.equal(expected); - }); - }); - - it('should always emit in the same order', function () { - const handler = sinon.stub(); - - const obj = new Events(); - obj.on('block', _.partial(handler, 'block')); - obj.on('last', _.partial(handler, 'last')); - - return Promise.all([ - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('last'), - ]).then(function () { - expect(handler.callCount).to.be(10); - handler.args.forEach(function (args, i) { - expect(args[0]).to.be(i < 9 ? 'block' : 'last'); - }); - }); - }); - - it('calls emitted handlers asynchronously', (done) => { - const listenerStub = sinon.stub(); - eventsInstance.on('test', listenerStub); - eventsInstance.emit('test'); - sinon.assert.notCalled(listenerStub); - - setTimeout(() => { - sinon.assert.calledOnce(listenerStub); - done(); - }, 100); - }); - - it('calling off after an emit that has not yet triggered the handler, will not call the handler', (done) => { - const listenerStub = sinon.stub(); - eventsInstance.on('test', listenerStub); - eventsInstance.emit('test'); - // It's called asynchronously so it shouldn't be called yet. - sinon.assert.notCalled(listenerStub); - eventsInstance.off('test', listenerStub); - - setTimeout(() => { - sinon.assert.notCalled(listenerStub); - done(); - }, 100); - }); -}); diff --git a/src/legacy/ui/public/__tests__/metadata.js b/src/legacy/ui/public/__tests__/metadata.js deleted file mode 100644 index c5051d70849cd..0000000000000 --- a/src/legacy/ui/public/__tests__/metadata.js +++ /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 expect from '@kbn/expect'; -import { metadata } from '../metadata'; -describe('ui/metadata', () => { - it('is immutable', () => { - expect(() => (metadata.foo = 'something')).to.throw; - expect(() => (metadata.version = 'something')).to.throw; - expect(() => (metadata.vars = {})).to.throw; - expect(() => (metadata.vars.kbnIndex = 'something')).to.throw; - }); -}); diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss deleted file mode 100644 index a441b773d4a4e..0000000000000 --- a/src/legacy/ui/public/_index.scss +++ /dev/null @@ -1,9 +0,0 @@ -// Prefix all styles with "kbn" to avoid conflicts. -// Examples -// kbnChart -// kbnChart__legend -// kbnChart__legend--small -// kbnChart__legend-isLoading - -@import './accessibility/index'; -@import './directives/index'; diff --git a/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js b/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js deleted file mode 100644 index f3b7ab29d8a14..0000000000000 --- a/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js +++ /dev/null @@ -1,127 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../kbn_accessible_click'; -import { keys } from '@elastic/eui'; - -describe('kbnAccessibleClick directive', () => { - let $compile; - let $rootScope; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(function (_$compile_, _$rootScope_) { - $compile = _$compile_; - $rootScope = _$rootScope_; - }) - ); - - describe('throws an error', () => { - it('when the element is a button', () => { - const html = ``; - expect(() => { - $compile(html)($rootScope); - }).to.throwError(/kbnAccessibleClick doesn't need to be used on a button./); - }); - - it('when the element is a link with an href', () => { - const html = ``; - expect(() => { - $compile(html)($rootScope); - }).to.throwError( - /kbnAccessibleClick doesn't need to be used on a link if it has a href attribute./ - ); - }); - - it(`when the element doesn't have an ng-click`, () => { - const html = `
`; - expect(() => { - $compile(html)($rootScope); - }).to.throwError(/kbnAccessibleClick requires ng-click to be defined on its element./); - }); - }); - - describe(`doesn't throw an error`, () => { - it('when the element is a link without an href', () => { - const html = ``; - expect(() => { - $compile(html)($rootScope); - }).not.to.throwError(); - }); - }); - - describe('adds accessibility attributes', () => { - it('tabindex', () => { - const html = `
`; - const element = $compile(html)($rootScope); - expect(element.attr('tabindex')).to.be('0'); - }); - - it('role', () => { - const html = `
`; - const element = $compile(html)($rootScope); - expect(element.attr('role')).to.be('button'); - }); - }); - - describe(`doesn't override pre-existing accessibility attributes`, () => { - it('tabindex', () => { - const html = `
`; - const element = $compile(html)($rootScope); - expect(element.attr('tabindex')).to.be('1'); - }); - - it('role', () => { - const html = `
`; - const element = $compile(html)($rootScope); - expect(element.attr('role')).to.be('submit'); - }); - }); - - describe(`calls ng-click`, () => { - let scope; - let element; - - beforeEach(function () { - scope = $rootScope.$new(); - scope.handleClick = sinon.stub(); - const html = `
`; - element = $compile(html)(scope); - }); - - it(`on ENTER keyup`, () => { - const e = angular.element.Event('keyup'); // eslint-disable-line new-cap - e.key = keys.ENTER; - element.trigger(e); - sinon.assert.calledOnce(scope.handleClick); - }); - - it(`on SPACE keyup`, () => { - const e = angular.element.Event('keyup'); // eslint-disable-line new-cap - e.key = keys.SPACE; - element.trigger(e); - sinon.assert.calledOnce(scope.handleClick); - }); - }); -}); diff --git a/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js b/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js deleted file mode 100644 index ce1bf95bf0fb7..0000000000000 --- a/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.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 angular from 'angular'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../kbn_ui_ace_keyboard_mode'; -import { keys } from '@elastic/eui'; - -describe('kbnUiAceKeyboardMode directive', () => { - let element; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(($compile, $rootScope) => { - element = $compile(`
`)($rootScope.$new()); - }) - ); - - it('should add the hint element', () => { - expect(element.find('.kbnUiAceKeyboardHint').length).to.be(1); - }); - - describe('hint element', () => { - it('should be tabable', () => { - expect(element.find('.kbnUiAceKeyboardHint').attr('tabindex')).to.be('0'); - }); - - it('should move focus to textbox and be inactive if pressed enter on it', () => { - const textarea = element.find('textarea'); - sinon.spy(textarea[0], 'focus'); - const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap - ev.key = keys.ENTER; - element.find('.kbnUiAceKeyboardHint').trigger(ev); - expect(textarea[0].focus.called).to.be(true); - expect( - element.find('.kbnUiAceKeyboardHint').hasClass('kbnUiAceKeyboardHint-isInactive') - ).to.be(true); - }); - - it('should be shown again, when pressing Escape in ace editor', () => { - const textarea = element.find('textarea'); - const hint = element.find('.kbnUiAceKeyboardHint'); - sinon.spy(hint[0], 'focus'); - const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap - ev.key = keys.ESCAPE; - textarea.trigger(ev); - expect(hint[0].focus.called).to.be(true); - expect(hint.hasClass('kbnUiAceKeyboardHint-isInactive')).to.be(false); - }); - }); - - describe('ui-ace textarea', () => { - it('should not be tabable anymore', () => { - expect(element.find('textarea').attr('tabindex')).to.be('-1'); - }); - }); -}); - -describe('kbnUiAceKeyboardModeService', () => { - let element; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(($compile, $rootScope, kbnUiAceKeyboardModeService) => { - const scope = $rootScope.$new(); - element = $compile(`
`)(scope); - kbnUiAceKeyboardModeService.initialize(scope, element); - }) - ); - - it('should add the hint element', () => { - expect(element.find('.kbnUiAceKeyboardHint').length).to.be(1); - }); - - describe('hint element', () => { - it('should be tabable', () => { - expect(element.find('.kbnUiAceKeyboardHint').attr('tabindex')).to.be('0'); - }); - - it('should move focus to textbox and be inactive if pressed enter on it', () => { - const textarea = element.find('textarea'); - sinon.spy(textarea[0], 'focus'); - const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap - ev.key = keys.ENTER; - element.find('.kbnUiAceKeyboardHint').trigger(ev); - expect(textarea[0].focus.called).to.be(true); - expect( - element.find('.kbnUiAceKeyboardHint').hasClass('kbnUiAceKeyboardHint-isInactive') - ).to.be(true); - }); - - it('should be shown again, when pressing Escape in ace editor', () => { - const textarea = element.find('textarea'); - const hint = element.find('.kbnUiAceKeyboardHint'); - sinon.spy(hint[0], 'focus'); - const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap - ev.key = keys.ESCAPE; - textarea.trigger(ev); - expect(hint[0].focus.called).to.be(true); - expect(hint.hasClass('kbnUiAceKeyboardHint-isInactive')).to.be(false); - }); - }); - - describe('ui-ace textarea', () => { - it('should not be tabable anymore', () => { - expect(element.find('textarea').attr('tabindex')).to.be('-1'); - }); - }); -}); diff --git a/src/legacy/ui/public/accessibility/__tests__/scrollto_activedescendant.js b/src/legacy/ui/public/accessibility/__tests__/scrollto_activedescendant.js deleted file mode 100644 index d5ccbf887e79b..0000000000000 --- a/src/legacy/ui/public/accessibility/__tests__/scrollto_activedescendant.js +++ /dev/null @@ -1,56 +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 ngMock from 'ng_mock'; -import '../scrollto_activedescendant'; - -describe('scrolltoActivedescendant directive', () => { - let $compile; - let $rootScope; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject((_$compile_, _$rootScope_) => { - $compile = _$compile_; - $rootScope = _$rootScope_; - }) - ); - - it('should call scrollIntoView on aria-activedescendant changes', () => { - const scope = $rootScope.$new(); - scope.ad = ''; - const element = $compile(`
- - -
`)(scope); - const child1 = element.find('#child1'); - const child2 = element.find('#child2'); - sinon.spy(child1[0], 'scrollIntoView'); - sinon.spy(child2[0], 'scrollIntoView'); - scope.ad = 'child1'; - scope.$digest(); - expect(child1[0].scrollIntoView.calledOnce).to.be.eql(true); - scope.ad = 'child2'; - scope.$digest(); - expect(child2[0].scrollIntoView.calledOnce).to.be.eql(true); - }); -}); diff --git a/src/legacy/ui/public/accessibility/_index.scss b/src/legacy/ui/public/accessibility/_index.scss deleted file mode 100644 index 95062449e4b13..0000000000000 --- a/src/legacy/ui/public/accessibility/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './kbn_ui_ace_keyboard_mode'; diff --git a/src/legacy/ui/public/accessibility/_kbn_ui_ace_keyboard_mode.scss b/src/legacy/ui/public/accessibility/_kbn_ui_ace_keyboard_mode.scss deleted file mode 100644 index 9ace600db4197..0000000000000 --- a/src/legacy/ui/public/accessibility/_kbn_ui_ace_keyboard_mode.scss +++ /dev/null @@ -1,24 +0,0 @@ -.kbnUiAceKeyboardHint { - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; - background: transparentize($euiColorEmptyShade, 0.3); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - opacity: 0; - - &:focus { - opacity: 1; - border: 2px solid $euiColorPrimary; - z-index: 1000; - } - - &.kbnUiAceKeyboardHint-isInactive { - display: none; - } -} diff --git a/src/legacy/ui/public/accessibility/angular_aria.js b/src/legacy/ui/public/accessibility/angular_aria.js deleted file mode 100644 index 4335eddf0d8cd..0000000000000 --- a/src/legacy/ui/public/accessibility/angular_aria.js +++ /dev/null @@ -1,39 +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-aria'; -import { uiModules } from '../modules'; - -/** - * This module will take care of attaching appropriate aria tags related to some angular stuff, - * e.g. it will attach aria-invalid if the model state is set to invalid. - * - * You can find more infos in the official documentation: https://docs.angularjs.org/api/ngAria. - * - * Three settings are disabled: it won't automatically attach `tabindex`, `role=button` or - * handling keyboard events for `ngClick` directives. Kibana uses `kbnAccessibleClick` to handle - * those cases where you need an `ngClick` non button element to have keyboard access. - */ -uiModules.get('kibana', ['ngAria']).config(($ariaProvider) => { - $ariaProvider.config({ - bindKeydown: false, - bindRoleForClick: false, - tabindex: false, - }); -}); diff --git a/src/legacy/ui/public/accessibility/index.js b/src/legacy/ui/public/accessibility/index.js deleted file mode 100644 index 5ff2521421866..0000000000000 --- a/src/legacy/ui/public/accessibility/index.js +++ /dev/null @@ -1,22 +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_aria'; -import './kbn_accessible_click'; -import './scrollto_activedescendant'; diff --git a/src/legacy/ui/public/accessibility/kbn_accessible_click.js b/src/legacy/ui/public/accessibility/kbn_accessible_click.js deleted file mode 100644 index a57fbc6be82fd..0000000000000 --- a/src/legacy/ui/public/accessibility/kbn_accessible_click.js +++ /dev/null @@ -1,43 +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. - */ - -/** - * Interactive elements must be able to receive focus. - * - * Ideally, this means using elements that are natively keyboard accessible (, - * , or - - -`; diff --git a/src/legacy/ui/public/exit_full_screen/_index.scss b/src/legacy/ui/public/exit_full_screen/_index.scss deleted file mode 100644 index 33dff05e2a687..0000000000000 --- a/src/legacy/ui/public/exit_full_screen/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../../../../plugins/kibana_react/public/exit_full_screen_button/index'; diff --git a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js deleted file mode 100644 index d4273c0fdb207..0000000000000 --- a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js +++ /dev/null @@ -1,88 +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. - */ - -jest.mock( - 'ui/chrome', - () => ({ - getKibanaVersion: () => '6.0.0', - setVisible: () => {}, - }), - { virtual: true } -); - -import React from 'react'; -import { mountWithIntl, renderWithIntl } from 'test_utils/enzyme_helpers'; -import sinon from 'sinon'; -import chrome from 'ui/chrome'; - -import { ExitFullScreenButton } from './exit_full_screen_button'; - -import { keys } from '@elastic/eui'; - -test('is rendered', () => { - const component = renderWithIntl( {}} />); - - expect(component).toMatchSnapshot(); -}); - -describe('onExitFullScreenMode', () => { - test('is called when the button is pressed', () => { - const onExitHandler = sinon.stub(); - - const component = mountWithIntl(); - - component.find('button').simulate('click'); - - sinon.assert.calledOnce(onExitHandler); - }); - - test('is called when the ESC key is pressed', () => { - const onExitHandler = sinon.stub(); - - mountWithIntl(); - - const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE }); - document.dispatchEvent(escapeKeyEvent); - - sinon.assert.calledOnce(onExitHandler); - }); -}); - -describe('chrome.setVisible', () => { - test('is called with false when the component is rendered', () => { - chrome.setVisible = sinon.stub(); - - const component = mountWithIntl( {}} />); - - component.find('button').simulate('click'); - - sinon.assert.calledOnce(chrome.setVisible); - sinon.assert.calledWith(chrome.setVisible, false); - }); - - test('is called with true the component is unmounted', () => { - const component = mountWithIntl( {}} />); - - chrome.setVisible = sinon.stub(); - component.unmount(); - - sinon.assert.calledOnce(chrome.setVisible); - sinon.assert.calledWith(chrome.setVisible, true); - }); -}); diff --git a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx deleted file mode 100644 index db4101010f6d6..0000000000000 --- a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx +++ /dev/null @@ -1,45 +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 React, { PureComponent } from 'react'; -import chrome from 'ui/chrome'; -import { ExitFullScreenButton as ExitFullScreenButtonUi } from '../../../../plugins/kibana_react/public'; - -/** - * DO NOT USE THIS COMPONENT, IT IS DEPRECATED. - * Use the one in `src/plugins/kibana_react`. - */ - -interface Props { - onExitFullScreenMode: () => void; -} - -export class ExitFullScreenButton extends PureComponent { - public UNSAFE_componentWillMount() { - chrome.setVisible(false); - } - - public componentWillUnmount() { - chrome.setVisible(true); - } - - public render() { - return ; - } -} diff --git a/src/legacy/ui/public/exit_full_screen/index.ts b/src/legacy/ui/public/exit_full_screen/index.ts deleted file mode 100644 index a965fd776e0c2..0000000000000 --- a/src/legacy/ui/public/exit_full_screen/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 { ExitFullScreenButton } from './exit_full_screen_button'; diff --git a/src/legacy/ui/public/flot-charts/API.md b/src/legacy/ui/public/flot-charts/API.md deleted file mode 100644 index 699e2500f4942..0000000000000 --- a/src/legacy/ui/public/flot-charts/API.md +++ /dev/null @@ -1,1498 +0,0 @@ -# Flot Reference # - -**Table of Contents** - -[Introduction](#introduction) -| [Data Format](#data-format) -| [Plot Options](#plot-options) -| [Customizing the legend](#customizing-the-legend) -| [Customizing the axes](#customizing-the-axes) -| [Multiple axes](#multiple-axes) -| [Time series data](#time-series-data) -| [Customizing the data series](#customizing-the-data-series) -| [Customizing the grid](#customizing-the-grid) -| [Specifying gradients](#specifying-gradients) -| [Plot Methods](#plot-methods) -| [Hooks](#hooks) -| [Plugins](#plugins) -| [Version number](#version-number) - ---- - -## Introduction ## - -Consider a call to the plot function: - -```js -var plot = $.plot(placeholder, data, options) -``` - -The placeholder is a jQuery object or DOM element or jQuery expression -that the plot will be put into. This placeholder needs to have its -width and height set as explained in the [README](README.md) (go read that now if -you haven't, it's short). The plot will modify some properties of the -placeholder so it's recommended you simply pass in a div that you -don't use for anything else. Make sure you check any fancy styling -you apply to the div, e.g. background images have been reported to be a -problem on IE 7. - -The plot function can also be used as a jQuery chainable property. This form -naturally can't return the plot object directly, but you can still access it -via the 'plot' data key, like this: - -```js -var plot = $("#placeholder").plot(data, options).data("plot"); -``` - -The format of the data is documented below, as is the available -options. The plot object returned from the call has some methods you -can call. These are documented separately below. - -Note that in general Flot gives no guarantees if you change any of the -objects you pass in to the plot function or get out of it since -they're not necessarily deep-copied. - - -## Data Format ## - -The data is an array of data series: - -```js -[ series1, series2, ... ] -``` - -A series can either be raw data or an object with properties. The raw -data format is an array of points: - -```js -[ [x1, y1], [x2, y2], ... ] -``` - -E.g. - -```js -[ [1, 3], [2, 14.01], [3.5, 3.14] ] -``` - -Note that to simplify the internal logic in Flot both the x and y -values must be numbers (even if specifying time series, see below for -how to do this). This is a common problem because you might retrieve -data from the database and serialize them directly to JSON without -noticing the wrong type. If you're getting mysterious errors, double -check that you're inputting numbers and not strings. - -If a null is specified as a point or if one of the coordinates is null -or couldn't be converted to a number, the point is ignored when -drawing. As a special case, a null value for lines is interpreted as a -line segment end, i.e. the points before and after the null value are -not connected. - -Lines and points take two coordinates. For filled lines and bars, you -can specify a third coordinate which is the bottom of the filled -area/bar (defaults to 0). - -The format of a single series object is as follows: - -```js -{ - color: color or number - data: rawdata - label: string - lines: specific lines options - bars: specific bars options - points: specific points options - xaxis: number - yaxis: number - clickable: boolean - hoverable: boolean - shadowSize: number - highlightColor: color or number -} -``` - -You don't have to specify any of them except the data, the rest are -options that will get default values. Typically you'd only specify -label and data, like this: - -```js -{ - label: "y = 3", - data: [[0, 3], [10, 3]] -} -``` - -The label is used for the legend, if you don't specify one, the series -will not show up in the legend. - -If you don't specify color, the series will get a color from the -auto-generated colors. The color is either a CSS color specification -(like "rgb(255, 100, 123)") or an integer that specifies which of -auto-generated colors to select, e.g. 0 will get color no. 0, etc. - -The latter is mostly useful if you let the user add and remove series, -in which case you can hard-code the color index to prevent the colors -from jumping around between the series. - -The "xaxis" and "yaxis" options specify which axis to use. The axes -are numbered from 1 (default), so { yaxis: 2} means that the series -should be plotted against the second y axis. - -"clickable" and "hoverable" can be set to false to disable -interactivity for specific series if interactivity is turned on in -the plot, see below. - -The rest of the options are all documented below as they are the same -as the default options passed in via the options parameter in the plot -command. When you specify them for a specific data series, they will -override the default options for the plot for that data series. - -Here's a complete example of a simple data specification: - -```js -[ { label: "Foo", data: [ [10, 1], [17, -14], [30, 5] ] }, - { label: "Bar", data: [ [11, 13], [19, 11], [30, -7] ] } -] -``` - - -## Plot Options ## - -All options are completely optional. They are documented individually -below, to change them you just specify them in an object, e.g. - -```js -var options = { - series: { - lines: { show: true }, - points: { show: true } - } -}; - -$.plot(placeholder, data, options); -``` - - -## Customizing the legend ## - -```js -legend: { - show: boolean - labelFormatter: null or (fn: string, series object -> string) - labelBoxBorderColor: color - noColumns: number - position: "ne" or "nw" or "se" or "sw" - margin: number of pixels or [x margin, y margin] - backgroundColor: null or color - backgroundOpacity: number between 0 and 1 - container: null or jQuery object/DOM element/jQuery expression - sorted: null/false, true, "ascending", "descending", "reverse", or a comparator -} -``` - -The legend is generated as a table with the data series labels and -small label boxes with the color of the series. If you want to format -the labels in some way, e.g. make them to links, you can pass in a -function for "labelFormatter". Here's an example that makes them -clickable: - -```js -labelFormatter: function(label, series) { - // series is the series object for the label - return '' + label + ''; -} -``` - -To prevent a series from showing up in the legend, simply have the function -return null. - -"noColumns" is the number of columns to divide the legend table into. -"position" specifies the overall placement of the legend within the -plot (top-right, top-left, etc.) and margin the distance to the plot -edge (this can be either a number or an array of two numbers like [x, -y]). "backgroundColor" and "backgroundOpacity" specifies the -background. The default is a partly transparent auto-detected -background. - -If you want the legend to appear somewhere else in the DOM, you can -specify "container" as a jQuery object/expression to put the legend -table into. The "position" and "margin" etc. options will then be -ignored. Note that Flot will overwrite the contents of the container. - -Legend entries appear in the same order as their series by default. If "sorted" -is "reverse" then they appear in the opposite order from their series. To sort -them alphabetically, you can specify true, "ascending" or "descending", where -true and "ascending" are equivalent. - -You can also provide your own comparator function that accepts two -objects with "label" and "color" properties, and returns zero if they -are equal, a positive value if the first is greater than the second, -and a negative value if the first is less than the second. - -```js -sorted: function(a, b) { - // sort alphabetically in ascending order - return a.label == b.label ? 0 : ( - a.label > b.label ? 1 : -1 - ) -} -``` - - -## Customizing the axes ## - -```js -xaxis, yaxis: { - show: null or true/false - position: "bottom" or "top" or "left" or "right" - mode: null or "time" ("time" requires jquery.flot.time.js plugin) - timezone: null, "browser" or timezone (only makes sense for mode: "time") - - color: null or color spec - tickColor: null or color spec - font: null or font spec object - - min: null or number - max: null or number - autoscaleMargin: null or number - - transform: null or fn: number -> number - inverseTransform: null or fn: number -> number - - ticks: null or number or ticks array or (fn: axis -> ticks array) - tickSize: number or array - minTickSize: number or array - tickFormatter: (fn: number, object -> string) or string - tickDecimals: null or number - - labelWidth: null or number - labelHeight: null or number - reserveSpace: null or true - - tickLength: null or number - - alignTicksWithAxis: null or number -} -``` - -All axes have the same kind of options. The following describes how to -configure one axis, see below for what to do if you've got more than -one x axis or y axis. - -If you don't set the "show" option (i.e. it is null), visibility is -auto-detected, i.e. the axis will show up if there's data associated -with it. You can override this by setting the "show" option to true or -false. - -The "position" option specifies where the axis is placed, bottom or -top for x axes, left or right for y axes. The "mode" option determines -how the data is interpreted, the default of null means as decimal -numbers. Use "time" for time series data; see the time series data -section. The time plugin (jquery.flot.time.js) is required for time -series support. - -The "color" option determines the color of the line and ticks for the axis, and -defaults to the grid color with transparency. For more fine-grained control you -can also set the color of the ticks separately with "tickColor". - -You can customize the font and color used to draw the axis tick labels with CSS -or directly via the "font" option. When "font" is null - the default - each -tick label is given the 'flot-tick-label' class. For compatibility with Flot -0.7 and earlier the labels are also given the 'tickLabel' class, but this is -deprecated and scheduled to be removed with the release of version 1.0.0. - -To enable more granular control over styles, labels are divided between a set -of text containers, with each holding the labels for one axis. These containers -are given the classes 'flot-[x|y]-axis', and 'flot-[x|y]#-axis', where '#' is -the number of the axis when there are multiple axes. For example, the x-axis -labels for a simple plot with only a single x-axis might look like this: - -```html -
-
January 2013
- ... -
-``` - -For direct control over label styles you can also provide "font" as an object -with this format: - -```js -{ - size: 11, - lineHeight: 13, - style: "italic", - weight: "bold", - family: "sans-serif", - variant: "small-caps", - color: "#545454" -} -``` - -The size and lineHeight must be expressed in pixels; CSS units such as 'em' -or 'smaller' are not allowed. - -The options "min"/"max" are the precise minimum/maximum value on the -scale. If you don't specify either of them, a value will automatically -be chosen based on the minimum/maximum data values. Note that Flot -always examines all the data values you feed to it, even if a -restriction on another axis may make some of them invisible (this -makes interactive use more stable). - -The "autoscaleMargin" is a bit esoteric: it's the fraction of margin -that the scaling algorithm will add to avoid that the outermost points -ends up on the grid border. Note that this margin is only applied when -a min or max value is not explicitly set. If a margin is specified, -the plot will furthermore extend the axis end-point to the nearest -whole tick. The default value is "null" for the x axes and 0.02 for y -axes which seems appropriate for most cases. - -"transform" and "inverseTransform" are callbacks you can put in to -change the way the data is drawn. You can design a function to -compress or expand certain parts of the axis non-linearly, e.g. -suppress weekends or compress far away points with a logarithm or some -other means. When Flot draws the plot, each value is first put through -the transform function. Here's an example, the x axis can be turned -into a natural logarithm axis with the following code: - -```js -xaxis: { - transform: function (v) { return Math.log(v); }, - inverseTransform: function (v) { return Math.exp(v); } -} -``` - -Similarly, for reversing the y axis so the values appear in inverse -order: - -```js -yaxis: { - transform: function (v) { return -v; }, - inverseTransform: function (v) { return -v; } -} -``` - -Note that for finding extrema, Flot assumes that the transform -function does not reorder values (it should be monotone). - -The inverseTransform is simply the inverse of the transform function -(so v == inverseTransform(transform(v)) for all relevant v). It is -required for converting from canvas coordinates to data coordinates, -e.g. for a mouse interaction where a certain pixel is clicked. If you -don't use any interactive features of Flot, you may not need it. - - -The rest of the options deal with the ticks. - -If you don't specify any ticks, a tick generator algorithm will make -some for you. The algorithm has two passes. It first estimates how -many ticks would be reasonable and uses this number to compute a nice -round tick interval size. Then it generates the ticks. - -You can specify how many ticks the algorithm aims for by setting -"ticks" to a number. The algorithm always tries to generate reasonably -round tick values so even if you ask for three ticks, you might get -five if that fits better with the rounding. If you don't want any -ticks at all, set "ticks" to 0 or an empty array. - -Another option is to skip the rounding part and directly set the tick -interval size with "tickSize". If you set it to 2, you'll get ticks at -2, 4, 6, etc. Alternatively, you can specify that you just don't want -ticks at a size less than a specific tick size with "minTickSize". -Note that for time series, the format is an array like [2, "month"], -see the next section. - -If you want to completely override the tick algorithm, you can specify -an array for "ticks", either like this: - -```js -ticks: [0, 1.2, 2.4] -``` - -Or like this where the labels are also customized: - -```js -ticks: [[0, "zero"], [1.2, "one mark"], [2.4, "two marks"]] -``` - -You can mix the two if you like. - -For extra flexibility you can specify a function as the "ticks" -parameter. The function will be called with an object with the axis -min and max and should return a ticks array. Here's a simplistic tick -generator that spits out intervals of pi, suitable for use on the x -axis for trigonometric functions: - -```js -function piTickGenerator(axis) { - var res = [], i = Math.floor(axis.min / Math.PI); - do { - var v = i * Math.PI; - res.push([v, i + "\u03c0"]); - ++i; - } while (v < axis.max); - return res; -} -``` - -You can control how the ticks look like with "tickDecimals", the -number of decimals to display (default is auto-detected). - -Alternatively, for ultimate control over how ticks are formatted you can -provide a function to "tickFormatter". The function is passed two -parameters, the tick value and an axis object with information, and -should return a string. The default formatter looks like this: - -```js -function formatter(val, axis) { - return val.toFixed(axis.tickDecimals); -} -``` - -The axis object has "min" and "max" with the range of the axis, -"tickDecimals" with the number of decimals to round the value to and -"tickSize" with the size of the interval between ticks as calculated -by the automatic axis scaling algorithm (or specified by you). Here's -an example of a custom formatter: - -```js -function suffixFormatter(val, axis) { - if (val > 1000000) - return (val / 1000000).toFixed(axis.tickDecimals) + " MB"; - else if (val > 1000) - return (val / 1000).toFixed(axis.tickDecimals) + " kB"; - else - return val.toFixed(axis.tickDecimals) + " B"; -} -``` - -"labelWidth" and "labelHeight" specifies a fixed size of the tick -labels in pixels. They're useful in case you need to align several -plots. "reserveSpace" means that even if an axis isn't shown, Flot -should reserve space for it - it is useful in combination with -labelWidth and labelHeight for aligning multi-axis charts. - -"tickLength" is the length of the tick lines in pixels. By default, the -innermost axes will have ticks that extend all across the plot, while -any extra axes use small ticks. A value of null means use the default, -while a number means small ticks of that length - set it to 0 to hide -the lines completely. - -If you set "alignTicksWithAxis" to the number of another axis, e.g. -alignTicksWithAxis: 1, Flot will ensure that the autogenerated ticks -of this axis are aligned with the ticks of the other axis. This may -improve the looks, e.g. if you have one y axis to the left and one to -the right, because the grid lines will then match the ticks in both -ends. The trade-off is that the forced ticks won't necessarily be at -natural places. - - -## Multiple axes ## - -If you need more than one x axis or y axis, you need to specify for -each data series which axis they are to use, as described under the -format of the data series, e.g. { data: [...], yaxis: 2 } specifies -that a series should be plotted against the second y axis. - -To actually configure that axis, you can't use the xaxis/yaxis options -directly - instead there are two arrays in the options: - -```js -xaxes: [] -yaxes: [] -``` - -Here's an example of configuring a single x axis and two y axes (we -can leave options of the first y axis empty as the defaults are fine): - -```js -{ - xaxes: [ { position: "top" } ], - yaxes: [ { }, { position: "right", min: 20 } ] -} -``` - -The arrays get their default values from the xaxis/yaxis settings, so -say you want to have all y axes start at zero, you can simply specify -yaxis: { min: 0 } instead of adding a min parameter to all the axes. - -Generally, the various interfaces in Flot dealing with data points -either accept an xaxis/yaxis parameter to specify which axis number to -use (starting from 1), or lets you specify the coordinate directly as -x2/x3/... or x2axis/x3axis/... instead of "x" or "xaxis". - - -## Time series data ## - -Please note that it is now required to include the time plugin, -jquery.flot.time.js, for time series support. - -Time series are a bit more difficult than scalar data because -calendars don't follow a simple base 10 system. For many cases, Flot -abstracts most of this away, but it can still be a bit difficult to -get the data into Flot. So we'll first discuss the data format. - -The time series support in Flot is based on JavaScript timestamps, -i.e. everywhere a time value is expected or handed over, a JavaScript -timestamp number is used. This is a number, not a Date object. A -JavaScript timestamp is the number of milliseconds since January 1, -1970 00:00:00 UTC. This is almost the same as Unix timestamps, except it's -in milliseconds, so remember to multiply by 1000! - -You can see a timestamp like this - -```js -alert((new Date()).getTime()) -``` - -There are different schools of thought when it comes to display of -timestamps. Many will want the timestamps to be displayed according to -a certain time zone, usually the time zone in which the data has been -produced. Some want the localized experience, where the timestamps are -displayed according to the local time of the visitor. Flot supports -both. Optionally you can include a third-party library to get -additional timezone support. - -Default behavior is that Flot always displays timestamps according to -UTC. The reason being that the core JavaScript Date object does not -support other fixed time zones. Often your data is at another time -zone, so it may take a little bit of tweaking to work around this -limitation. - -The easiest way to think about it is to pretend that the data -production time zone is UTC, even if it isn't. So if you have a -datapoint at 2002-02-20 08:00, you can generate a timestamp for eight -o'clock UTC even if it really happened eight o'clock UTC+0200. - -In PHP you can get an appropriate timestamp with: - -```php -strtotime("2002-02-20 UTC") * 1000 -``` - -In Python you can get it with something like: - -```python -calendar.timegm(datetime_object.timetuple()) * 1000 -``` -In Ruby you can get it using the `#to_i` method on the -[`Time`](http://apidock.com/ruby/Time/to_i) object. If you're using the -`active_support` gem (default for Ruby on Rails applications) `#to_i` is also -available on the `DateTime` and `ActiveSupport::TimeWithZone` objects. You -simply need to multiply the result by 1000: - -```ruby -Time.now.to_i * 1000 # => 1383582043000 -# ActiveSupport examples: -DateTime.now.to_i * 1000 # => 1383582043000 -ActiveSupport::TimeZone.new('Asia/Shanghai').now.to_i * 1000 -# => 1383582043000 -``` - -In .NET you can get it with something like: - -```aspx -public static int GetJavaScriptTimestamp(System.DateTime input) -{ - System.TimeSpan span = new System.TimeSpan(System.DateTime.Parse("1/1/1970").Ticks); - System.DateTime time = input.Subtract(span); - return (long)(time.Ticks / 10000); -} -``` - -JavaScript also has some support for parsing date strings, so it is -possible to generate the timestamps manually client-side. - -If you've already got the real UTC timestamp, it's too late to use the -pretend trick described above. But you can fix up the timestamps by -adding the time zone offset, e.g. for UTC+0200 you would add 2 hours -to the UTC timestamp you got. Then it'll look right on the plot. Most -programming environments have some means of getting the timezone -offset for a specific date (note that you need to get the offset for -each individual timestamp to account for daylight savings). - -The alternative with core JavaScript is to interpret the timestamps -according to the time zone that the visitor is in, which means that -the ticks will shift with the time zone and daylight savings of each -visitor. This behavior is enabled by setting the axis option -"timezone" to the value "browser". - -If you need more time zone functionality than this, there is still -another option. If you include the "timezone-js" library - in the page and set axis.timezone -to a value recognized by said library, Flot will use timezone-js to -interpret the timestamps according to that time zone. - -Once you've gotten the timestamps into the data and specified "time" -as the axis mode, Flot will automatically generate relevant ticks and -format them. As always, you can tweak the ticks via the "ticks" option -- just remember that the values should be timestamps (numbers), not -Date objects. - -Tick generation and formatting can also be controlled separately -through the following axis options: - -```js -minTickSize: array -timeformat: null or format string -monthNames: null or array of size 12 of strings -dayNames: null or array of size 7 of strings -twelveHourClock: boolean -``` - -Here "timeformat" is a format string to use. You might use it like -this: - -```js -xaxis: { - mode: "time", - timeformat: "%Y/%m/%d" -} -``` - -This will result in tick labels like "2000/12/24". A subset of the -standard strftime specifiers are supported (plus the nonstandard %q): - -```js -%a: weekday name (customizable) -%b: month name (customizable) -%d: day of month, zero-padded (01-31) -%e: day of month, space-padded ( 1-31) -%H: hours, 24-hour time, zero-padded (00-23) -%I: hours, 12-hour time, zero-padded (01-12) -%m: month, zero-padded (01-12) -%M: minutes, zero-padded (00-59) -%q: quarter (1-4) -%S: seconds, zero-padded (00-59) -%y: year (two digits) -%Y: year (four digits) -%p: am/pm -%P: AM/PM (uppercase version of %p) -%w: weekday as number (0-6, 0 being Sunday) -``` - -Flot 0.8 switched from %h to the standard %H hours specifier. The %h specifier -is still available, for backwards-compatibility, but is deprecated and -scheduled to be removed permanently with the release of version 1.0. - -You can customize the month names with the "monthNames" option. For -instance, for Danish you might specify: - -```js -monthNames: ["jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"] -``` - -Similarly you can customize the weekday names with the "dayNames" -option. An example in French: - -```js -dayNames: ["dim", "lun", "mar", "mer", "jeu", "ven", "sam"] -``` - -If you set "twelveHourClock" to true, the autogenerated timestamps -will use 12 hour AM/PM timestamps instead of 24 hour. This only -applies if you have not set "timeformat". Use the "%I" and "%p" or -"%P" options if you want to build your own format string with 12-hour -times. - -If the Date object has a strftime property (and it is a function), it -will be used instead of the built-in formatter. Thus you can include -a strftime library such as http://hacks.bluesmoon.info/strftime/ for -more powerful date/time formatting. - -If everything else fails, you can control the formatting by specifying -a custom tick formatter function as usual. Here's a simple example -which will format December 24 as 24/12: - -```js -tickFormatter: function (val, axis) { - var d = new Date(val); - return d.getUTCDate() + "/" + (d.getUTCMonth() + 1); -} -``` - -Note that for the time mode "tickSize" and "minTickSize" are a bit -special in that they are arrays on the form "[value, unit]" where unit -is one of "second", "minute", "hour", "day", "month" and "year". So -you can specify - -```js -minTickSize: [1, "month"] -``` - -to get a tick interval size of at least 1 month and correspondingly, -if axis.tickSize is [2, "day"] in the tick formatter, the ticks have -been produced with two days in-between. - - -## Customizing the data series ## - -```js -series: { - lines, points, bars: { - show: boolean - lineWidth: number - fill: boolean or number - fillColor: null or color/gradient - } - - lines, bars: { - zero: boolean - } - - points: { - radius: number - symbol: "circle" or function - } - - bars: { - barWidth: number - align: "left", "right" or "center" - horizontal: boolean - } - - lines: { - steps: boolean - } - - shadowSize: number - highlightColor: color or number -} - -colors: [ color1, color2, ... ] -``` - -The options inside "series: {}" are copied to each of the series. So -you can specify that all series should have bars by putting it in the -global options, or override it for individual series by specifying -bars in a particular the series object in the array of data. - -The most important options are "lines", "points" and "bars" that -specify whether and how lines, points and bars should be shown for -each data series. In case you don't specify anything at all, Flot will -default to showing lines (you can turn this off with -lines: { show: false }). You can specify the various types -independently of each other, and Flot will happily draw each of them -in turn (this is probably only useful for lines and points), e.g. - -```js -var options = { - series: { - lines: { show: true, fill: true, fillColor: "rgba(255, 255, 255, 0.8)" }, - points: { show: true, fill: false } - } -}; -``` - -"lineWidth" is the thickness of the line or outline in pixels. You can -set it to 0 to prevent a line or outline from being drawn; this will -also hide the shadow. - -"fill" is whether the shape should be filled. For lines, this produces -area graphs. You can use "fillColor" to specify the color of the fill. -If "fillColor" evaluates to false (default for everything except -points which are filled with white), the fill color is auto-set to the -color of the data series. You can adjust the opacity of the fill by -setting fill to a number between 0 (fully transparent) and 1 (fully -opaque). - -For bars, fillColor can be a gradient, see the gradient documentation -below. "barWidth" is the width of the bars in units of the x axis (or -the y axis if "horizontal" is true), contrary to most other measures -that are specified in pixels. For instance, for time series the unit -is milliseconds so 24 * 60 * 60 * 1000 produces bars with the width of -a day. "align" specifies whether a bar should be left-aligned -(default), right-aligned or centered on top of the value it represents. -When "horizontal" is on, the bars are drawn horizontally, i.e. from the -y axis instead of the x axis; note that the bar end points are still -defined in the same way so you'll probably want to swap the -coordinates if you've been plotting vertical bars first. - -Area and bar charts normally start from zero, regardless of the data's range. -This is because they convey information through size, and starting from a -different value would distort their meaning. In cases where the fill is purely -for decorative purposes, however, "zero" allows you to override this behavior. -It defaults to true for filled lines and bars; setting it to false tells the -series to use the same automatic scaling as an un-filled line. - -For lines, "steps" specifies whether two adjacent data points are -connected with a straight (possibly diagonal) line or with first a -horizontal and then a vertical line. Note that this transforms the -data by adding extra points. - -For points, you can specify the radius and the symbol. The only -built-in symbol type is circles, for other types you can use a plugin -or define them yourself by specifying a callback: - -```js -function cross(ctx, x, y, radius, shadow) { - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); -} -``` - -The parameters are the drawing context, x and y coordinates of the -center of the point, a radius which corresponds to what the circle -would have used and whether the call is to draw a shadow (due to -limited canvas support, shadows are currently faked through extra -draws). It's good practice to ensure that the area covered by the -symbol is the same as for the circle with the given radius, this -ensures that all symbols have approximately the same visual weight. - -"shadowSize" is the default size of shadows in pixels. Set it to 0 to -remove shadows. - -"highlightColor" is the default color of the translucent overlay used -to highlight the series when the mouse hovers over it. - -The "colors" array specifies a default color theme to get colors for -the data series from. You can specify as many colors as you like, like -this: - -```js -colors: ["#d18b2c", "#dba255", "#919733"] -``` - -If there are more data series than colors, Flot will try to generate -extra colors by lightening and darkening colors in the theme. - - -## Customizing the grid ## - -```js -grid: { - show: boolean - aboveData: boolean - color: color - backgroundColor: color/gradient or null - margin: number or margin object - labelMargin: number - axisMargin: number - markings: array of markings or (fn: axes -> array of markings) - borderWidth: number or object with "top", "right", "bottom" and "left" properties with different widths - borderColor: color or null or object with "top", "right", "bottom" and "left" properties with different colors - minBorderMargin: number or null - clickable: boolean - hoverable: boolean - autoHighlight: boolean - mouseActiveRadius: number -} - -interaction: { - redrawOverlayInterval: number or -1 -} -``` - -The grid is the thing with the axes and a number of ticks. Many of the -things in the grid are configured under the individual axes, but not -all. "color" is the color of the grid itself whereas "backgroundColor" -specifies the background color inside the grid area, here null means -that the background is transparent. You can also set a gradient, see -the gradient documentation below. - -You can turn off the whole grid including tick labels by setting -"show" to false. "aboveData" determines whether the grid is drawn -above the data or below (below is default). - -"margin" is the space in pixels between the canvas edge and the grid, -which can be either a number or an object with individual margins for -each side, in the form: - -```js -margin: { - top: top margin in pixels - left: left margin in pixels - bottom: bottom margin in pixels - right: right margin in pixels -} -``` - -"labelMargin" is the space in pixels between tick labels and axis -line, and "axisMargin" is the space in pixels between axes when there -are two next to each other. - -"borderWidth" is the width of the border around the plot. Set it to 0 -to disable the border. Set it to an object with "top", "right", -"bottom" and "left" properties to use different widths. You can -also set "borderColor" if you want the border to have a different color -than the grid lines. Set it to an object with "top", "right", "bottom" -and "left" properties to use different colors. "minBorderMargin" controls -the default minimum margin around the border - it's used to make sure -that points aren't accidentally clipped by the canvas edge so by default -the value is computed from the point radius. - -"markings" is used to draw simple lines and rectangular areas in the -background of the plot. You can either specify an array of ranges on -the form { xaxis: { from, to }, yaxis: { from, to } } (with multiple -axes, you can specify coordinates for other axes instead, e.g. as -x2axis/x3axis/...) or with a function that returns such an array given -the axes for the plot in an object as the first parameter. - -You can set the color of markings by specifying "color" in the ranges -object. Here's an example array: - -```js -markings: [ { xaxis: { from: 0, to: 2 }, yaxis: { from: 10, to: 10 }, color: "#bb0000" }, ... ] -``` - -If you leave out one of the values, that value is assumed to go to the -border of the plot. So for example if you only specify { xaxis: { -from: 0, to: 2 } } it means an area that extends from the top to the -bottom of the plot in the x range 0-2. - -A line is drawn if from and to are the same, e.g. - -```js -markings: [ { yaxis: { from: 1, to: 1 } }, ... ] -``` - -would draw a line parallel to the x axis at y = 1. You can control the -line width with "lineWidth" in the range object. - -An example function that makes vertical stripes might look like this: - -```js -markings: function (axes) { - var markings = []; - for (var x = Math.floor(axes.xaxis.min); x < axes.xaxis.max; x += 2) - markings.push({ xaxis: { from: x, to: x + 1 } }); - return markings; -} -``` - -If you set "clickable" to true, the plot will listen for click events -on the plot area and fire a "plotclick" event on the placeholder with -a position and a nearby data item object as parameters. The coordinates -are available both in the unit of the axes (not in pixels) and in -global screen coordinates. - -Likewise, if you set "hoverable" to true, the plot will listen for -mouse move events on the plot area and fire a "plothover" event with -the same parameters as the "plotclick" event. If "autoHighlight" is -true (the default), nearby data items are highlighted automatically. -If needed, you can disable highlighting and control it yourself with -the highlight/unhighlight plot methods described elsewhere. - -You can use "plotclick" and "plothover" events like this: - -```js -$.plot($("#placeholder"), [ d ], { grid: { clickable: true } }); - -$("#placeholder").bind("plotclick", function (event, pos, item) { - alert("You clicked at " + pos.x + ", " + pos.y); - // axis coordinates for other axes, if present, are in pos.x2, pos.x3, ... - // if you need global screen coordinates, they are pos.pageX, pos.pageY - - if (item) { - highlight(item.series, item.datapoint); - alert("You clicked a point!"); - } -}); -``` - -The item object in this example is either null or a nearby object on the form: - -```js -item: { - datapoint: the point, e.g. [0, 2] - dataIndex: the index of the point in the data array - series: the series object - seriesIndex: the index of the series - pageX, pageY: the global screen coordinates of the point -} -``` - -For instance, if you have specified the data like this - -```js -$.plot($("#placeholder"), [ { label: "Foo", data: [[0, 10], [7, 3]] } ], ...); -``` - -and the mouse is near the point (7, 3), "datapoint" is [7, 3], -"dataIndex" will be 1, "series" is a normalized series object with -among other things the "Foo" label in series.label and the color in -series.color, and "seriesIndex" is 0. Note that plugins and options -that transform the data can shift the indexes from what you specified -in the original data array. - -If you use the above events to update some other information and want -to clear out that info in case the mouse goes away, you'll probably -also need to listen to "mouseout" events on the placeholder div. - -"mouseActiveRadius" specifies how far the mouse can be from an item -and still activate it. If there are two or more points within this -radius, Flot chooses the closest item. For bars, the top-most bar -(from the latest specified data series) is chosen. - -If you want to disable interactivity for a specific data series, you -can set "hoverable" and "clickable" to false in the options for that -series, like this: - -```js -{ data: [...], label: "Foo", clickable: false } -``` - -"redrawOverlayInterval" specifies the maximum time to delay a redraw -of interactive things (this works as a rate limiting device). The -default is capped to 60 frames per second. You can set it to -1 to -disable the rate limiting. - - -## Specifying gradients ## - -A gradient is specified like this: - -```js -{ colors: [ color1, color2, ... ] } -``` - -For instance, you might specify a background on the grid going from -black to gray like this: - -```js -grid: { - backgroundColor: { colors: ["#000", "#999"] } -} -``` - -For the series you can specify the gradient as an object that -specifies the scaling of the brightness and the opacity of the series -color, e.g. - -```js -{ colors: [{ opacity: 0.8 }, { brightness: 0.6, opacity: 0.8 } ] } -``` - -where the first color simply has its alpha scaled, whereas the second -is also darkened. For instance, for bars the following makes the bars -gradually disappear, without outline: - -```js -bars: { - show: true, - lineWidth: 0, - fill: true, - fillColor: { colors: [ { opacity: 0.8 }, { opacity: 0.1 } ] } -} -``` - -Flot currently only supports vertical gradients drawn from top to -bottom because that's what works with IE. - - -## Plot Methods ## - -The Plot object returned from the plot function has some methods you -can call: - - - highlight(series, datapoint) - - Highlight a specific datapoint in the data series. You can either - specify the actual objects, e.g. if you got them from a - "plotclick" event, or you can specify the indices, e.g. - highlight(1, 3) to highlight the fourth point in the second series - (remember, zero-based indexing). - - - unhighlight(series, datapoint) or unhighlight() - - Remove the highlighting of the point, same parameters as - highlight. - - If you call unhighlight with no parameters, e.g. as - plot.unhighlight(), all current highlights are removed. - - - setData(data) - - You can use this to reset the data used. Note that axis scaling, - ticks, legend etc. will not be recomputed (use setupGrid() to do - that). You'll probably want to call draw() afterwards. - - You can use this function to speed up redrawing a small plot if - you know that the axes won't change. Put in the new data with - setData(newdata), call draw(), and you're good to go. Note that - for large datasets, almost all the time is consumed in draw() - plotting the data so in this case don't bother. - - - setupGrid() - - Recalculate and set axis scaling, ticks, legend etc. - - Note that because of the drawing model of the canvas, this - function will immediately redraw (actually reinsert in the DOM) - the labels and the legend, but not the actual tick lines because - they're drawn on the canvas. You need to call draw() to get the - canvas redrawn. - - - draw() - - Redraws the plot canvas. - - - triggerRedrawOverlay() - - Schedules an update of an overlay canvas used for drawing - interactive things like a selection and point highlights. This - is mostly useful for writing plugins. The redraw doesn't happen - immediately, instead a timer is set to catch multiple successive - redraws (e.g. from a mousemove). You can get to the overlay by - setting up a drawOverlay hook. - - - width()/height() - - Gets the width and height of the plotting area inside the grid. - This is smaller than the canvas or placeholder dimensions as some - extra space is needed (e.g. for labels). - - - offset() - - Returns the offset of the plotting area inside the grid relative - to the document, useful for instance for calculating mouse - positions (event.pageX/Y minus this offset is the pixel position - inside the plot). - - - pointOffset({ x: xpos, y: ypos }) - - Returns the calculated offset of the data point at (x, y) in data - space within the placeholder div. If you are working with multiple - axes, you can specify the x and y axis references, e.g. - - ```js - o = pointOffset({ x: xpos, y: ypos, xaxis: 2, yaxis: 3 }) - // o.left and o.top now contains the offset within the div - ```` - - - resize() - - Tells Flot to resize the drawing canvas to the size of the - placeholder. You need to run setupGrid() and draw() afterwards as - canvas resizing is a destructive operation. This is used - internally by the resize plugin. - - - shutdown() - - Cleans up any event handlers Flot has currently registered. This - is used internally. - -There are also some members that let you peek inside the internal -workings of Flot which is useful in some cases. Note that if you change -something in the objects returned, you're changing the objects used by -Flot to keep track of its state, so be careful. - - - getData() - - Returns an array of the data series currently used in normalized - form with missing settings filled in according to the global - options. So for instance to find out what color Flot has assigned - to the data series, you could do this: - - ```js - var series = plot.getData(); - for (var i = 0; i < series.length; ++i) - alert(series[i].color); - ``` - - A notable other interesting field besides color is datapoints - which has a field "points" with the normalized data points in a - flat array (the field "pointsize" is the increment in the flat - array to get to the next point so for a dataset consisting only of - (x,y) pairs it would be 2). - - - getAxes() - - Gets an object with the axes. The axes are returned as the - attributes of the object, so for instance getAxes().xaxis is the - x axis. - - Various things are stuffed inside an axis object, e.g. you could - use getAxes().xaxis.ticks to find out what the ticks are for the - xaxis. Two other useful attributes are p2c and c2p, functions for - transforming from data point space to the canvas plot space and - back. Both returns values that are offset with the plot offset. - Check the Flot source code for the complete set of attributes (or - output an axis with console.log() and inspect it). - - With multiple axes, the extra axes are returned as x2axis, x3axis, - etc., e.g. getAxes().y2axis is the second y axis. You can check - y2axis.used to see whether the axis is associated with any data - points and y2axis.show to see if it is currently shown. - - - getPlaceholder() - - Returns placeholder that the plot was put into. This can be useful - for plugins for adding DOM elements or firing events. - - - getCanvas() - - Returns the canvas used for drawing in case you need to hack on it - yourself. You'll probably need to get the plot offset too. - - - getPlotOffset() - - Gets the offset that the grid has within the canvas as an object - with distances from the canvas edges as "left", "right", "top", - "bottom". I.e., if you draw a circle on the canvas with the center - placed at (left, top), its center will be at the top-most, left - corner of the grid. - - - getOptions() - - Gets the options for the plot, normalized, with default values - filled in. You get a reference to actual values used by Flot, so - if you modify the values in here, Flot will use the new values. - If you change something, you probably have to call draw() or - setupGrid() or triggerRedrawOverlay() to see the change. - - -## Hooks ## - -In addition to the public methods, the Plot object also has some hooks -that can be used to modify the plotting process. You can install a -callback function at various points in the process, the function then -gets access to the internal data structures in Flot. - -Here's an overview of the phases Flot goes through: - - 1. Plugin initialization, parsing options - - 2. Constructing the canvases used for drawing - - 3. Set data: parsing data specification, calculating colors, - copying raw data points into internal format, - normalizing them, finding max/min for axis auto-scaling - - 4. Grid setup: calculating axis spacing, ticks, inserting tick - labels, the legend - - 5. Draw: drawing the grid, drawing each of the series in turn - - 6. Setting up event handling for interactive features - - 7. Responding to events, if any - - 8. Shutdown: this mostly happens in case a plot is overwritten - -Each hook is simply a function which is put in the appropriate array. -You can add them through the "hooks" option, and they are also available -after the plot is constructed as the "hooks" attribute on the returned -plot object, e.g. - -```js - // define a simple draw hook - function hellohook(plot, canvascontext) { alert("hello!"); }; - - // pass it in, in an array since we might want to specify several - var plot = $.plot(placeholder, data, { hooks: { draw: [hellohook] } }); - - // we can now find it again in plot.hooks.draw[0] unless a plugin - // has added other hooks -``` - -The available hooks are described below. All hook callbacks get the -plot object as first parameter. You can find some examples of defined -hooks in the plugins bundled with Flot. - - - processOptions [phase 1] - - ```function(plot, options)``` - - Called after Flot has parsed and merged options. Useful in the - instance where customizations beyond simple merging of default - values is needed. A plugin might use it to detect that it has been - enabled and then turn on or off other options. - - - - processRawData [phase 3] - - ```function(plot, series, data, datapoints)``` - - Called before Flot copies and normalizes the raw data for the given - series. If the function fills in datapoints.points with normalized - points and sets datapoints.pointsize to the size of the points, - Flot will skip the copying/normalization step for this series. - - In any case, you might be interested in setting datapoints.format, - an array of objects for specifying how a point is normalized and - how it interferes with axis scaling. It accepts the following options: - - ```js - { - x, y: boolean, - number: boolean, - required: boolean, - defaultValue: value, - autoscale: boolean - } - ``` - - "x" and "y" specify whether the value is plotted against the x or y axis, - and is currently used only to calculate axis min-max ranges. The default - format array, for example, looks like this: - - ```js - [ - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ] - ``` - - This indicates that a point, i.e. [0, 25], consists of two values, with the - first being plotted on the x axis and the second on the y axis. - - If "number" is true, then the value must be numeric, and is set to null if - it cannot be converted to a number. - - "defaultValue" provides a fallback in case the original value is null. This - is for instance handy for bars, where one can omit the third coordinate - (the bottom of the bar), which then defaults to zero. - - If "required" is true, then the value must exist (be non-null) for the - point as a whole to be valid. If no value is provided, then the entire - point is cleared out with nulls, turning it into a gap in the series. - - "autoscale" determines whether the value is considered when calculating an - automatic min-max range for the axes that the value is plotted against. - - - processDatapoints [phase 3] - - ```function(plot, series, datapoints)``` - - Called after normalization of the given series but before finding - min/max of the data points. This hook is useful for implementing data - transformations. "datapoints" contains the normalized data points in - a flat array as datapoints.points with the size of a single point - given in datapoints.pointsize. Here's a simple transform that - multiplies all y coordinates by 2: - - ```js - function multiply(plot, series, datapoints) { - var points = datapoints.points, ps = datapoints.pointsize; - for (var i = 0; i < points.length; i += ps) - points[i + 1] *= 2; - } - ``` - - Note that you must leave datapoints in a good condition as Flot - doesn't check it or do any normalization on it afterwards. - - - processOffset [phase 4] - - ```function(plot, offset)``` - - Called after Flot has initialized the plot's offset, but before it - draws any axes or plot elements. This hook is useful for customizing - the margins between the grid and the edge of the canvas. "offset" is - an object with attributes "top", "bottom", "left" and "right", - corresponding to the margins on the four sides of the plot. - - - drawBackground [phase 5] - - ```function(plot, canvascontext)``` - - Called before all other drawing operations. Used to draw backgrounds - or other custom elements before the plot or axes have been drawn. - - - drawSeries [phase 5] - - ```function(plot, canvascontext, series)``` - - Hook for custom drawing of a single series. Called just before the - standard drawing routine has been called in the loop that draws - each series. - - - draw [phase 5] - - ```function(plot, canvascontext)``` - - Hook for drawing on the canvas. Called after the grid is drawn - (unless it's disabled or grid.aboveData is set) and the series have - been plotted (in case any points, lines or bars have been turned - on). For examples of how to draw things, look at the source code. - - - bindEvents [phase 6] - - ```function(plot, eventHolder)``` - - Called after Flot has setup its event handlers. Should set any - necessary event handlers on eventHolder, a jQuery object with the - canvas, e.g. - - ```js - function (plot, eventHolder) { - eventHolder.mousedown(function (e) { - alert("You pressed the mouse at " + e.pageX + " " + e.pageY); - }); - } - ``` - - Interesting events include click, mousemove, mouseup/down. You can - use all jQuery events. Usually, the event handlers will update the - state by drawing something (add a drawOverlay hook and call - triggerRedrawOverlay) or firing an externally visible event for - user code. See the crosshair plugin for an example. - - Currently, eventHolder actually contains both the static canvas - used for the plot itself and the overlay canvas used for - interactive features because some versions of IE get the stacking - order wrong. The hook only gets one event, though (either for the - overlay or for the static canvas). - - Note that custom plot events generated by Flot are not generated on - eventHolder, but on the div placeholder supplied as the first - argument to the plot call. You can get that with - plot.getPlaceholder() - that's probably also the one you should use - if you need to fire a custom event. - - - drawOverlay [phase 7] - - ```function (plot, canvascontext)``` - - The drawOverlay hook is used for interactive things that need a - canvas to draw on. The model currently used by Flot works the way - that an extra overlay canvas is positioned on top of the static - canvas. This overlay is cleared and then completely redrawn - whenever something interesting happens. This hook is called when - the overlay canvas is to be redrawn. - - "canvascontext" is the 2D context of the overlay canvas. You can - use this to draw things. You'll most likely need some of the - metrics computed by Flot, e.g. plot.width()/plot.height(). See the - crosshair plugin for an example. - - - shutdown [phase 8] - - ```function (plot, eventHolder)``` - - Run when plot.shutdown() is called, which usually only happens in - case a plot is overwritten by a new plot. If you're writing a - plugin that adds extra DOM elements or event handlers, you should - add a callback to clean up after you. Take a look at the section in - the [PLUGINS](PLUGINS.md) document for more info. - - -## Plugins ## - -Plugins extend the functionality of Flot. To use a plugin, simply -include its JavaScript file after Flot in the HTML page. - -If you're worried about download size/latency, you can concatenate all -the plugins you use, and Flot itself for that matter, into one big file -(make sure you get the order right), then optionally run it through a -JavaScript minifier such as YUI Compressor. - -Here's a brief explanation of how the plugin plumbings work: - -Each plugin registers itself in the global array $.plot.plugins. When -you make a new plot object with $.plot, Flot goes through this array -calling the "init" function of each plugin and merging default options -from the "option" attribute of the plugin. The init function gets a -reference to the plot object created and uses this to register hooks -and add new public methods if needed. - -See the [PLUGINS](PLUGINS.md) document for details on how to write a plugin. As the -above description hints, it's actually pretty easy. - - -## Version number ## - -The version number of Flot is available in ```$.plot.version```. diff --git a/src/legacy/ui/public/flot-charts/index.js b/src/legacy/ui/public/flot-charts/index.js deleted file mode 100644 index f98aca30d2dec..0000000000000 --- a/src/legacy/ui/public/flot-charts/index.js +++ /dev/null @@ -1,42 +0,0 @@ -/* @notice - * - * This product includes code that is based on flot-charts, which was available - * under a "MIT" license. - * - * The MIT License (MIT) - * - * Copyright (c) 2007-2014 IOLA and Ole Laursen - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -import $ from 'jquery'; -if (window) window.jQuery = $; -require('ui/flot-charts/jquery.flot'); -require('ui/flot-charts/jquery.flot.time'); -require('ui/flot-charts/jquery.flot.canvas'); -require('ui/flot-charts/jquery.flot.symbol'); -require('ui/flot-charts/jquery.flot.crosshair'); -require('ui/flot-charts/jquery.flot.selection'); -require('ui/flot-charts/jquery.flot.pie'); -require('ui/flot-charts/jquery.flot.stack'); -require('ui/flot-charts/jquery.flot.threshold'); -require('ui/flot-charts/jquery.flot.fillbetween'); -require('ui/flot-charts/jquery.flot.log'); -module.exports = $; diff --git a/src/legacy/ui/public/flot-charts/jquery.colorhelpers.js b/src/legacy/ui/public/flot-charts/jquery.colorhelpers.js deleted file mode 100644 index b2f6dc4e433a3..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.colorhelpers.js +++ /dev/null @@ -1,180 +0,0 @@ -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ - -(function($) { - $.color = {}; - - // construct color object with some convenient chainable helpers - $.color.make = function (r, g, b, a) { - var o = {}; - o.r = r || 0; - o.g = g || 0; - o.b = b || 0; - o.a = a != null ? a : 1; - - o.add = function (c, d) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] += d; - return o.normalize(); - }; - - o.scale = function (c, f) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] *= f; - return o.normalize(); - }; - - o.toString = function () { - if (o.a >= 1.0) { - return "rgb("+[o.r, o.g, o.b].join(",")+")"; - } else { - return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")"; - } - }; - - o.normalize = function () { - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - o.r = clamp(0, parseInt(o.r), 255); - o.g = clamp(0, parseInt(o.g), 255); - o.b = clamp(0, parseInt(o.b), 255); - o.a = clamp(0, o.a, 1); - return o; - }; - - o.clone = function () { - return $.color.make(o.r, o.b, o.g, o.a); - }; - - return o.normalize(); - } - - // extract CSS color property from element, going up in the DOM - // if it's "transparent" - $.color.extract = function (elem, css) { - var c; - - do { - c = elem.css(css).toLowerCase(); - // keep going until we find an element that has color, or - // we hit the body or root (have no parent) - if (c != '' && c != 'transparent') - break; - elem = elem.parent(); - } while (elem.length && !$.nodeName(elem.get(0), "body")); - - // catch Safari's way of signalling transparent - if (c == "rgba(0, 0, 0, 0)") - c = "transparent"; - - return $.color.parse(c); - } - - // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), - // returns color object, if parsing failed, you get black (0, 0, - // 0) out - $.color.parse = function (str) { - var res, m = $.color.make; - - // Look for rgb(num,num,num) - if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); - - // Look for rgba(num,num,num,num) - if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); - - // Look for rgb(num%,num%,num%) - if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55); - - // Look for rgba(num%,num%,num%,num) - if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4])); - - // Look for #a0b1c2 - if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) - return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); - - // Look for #fff - if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) - return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); - - // Otherwise, we're most likely dealing with a named color - var name = $.trim(str).toLowerCase(); - if (name == "transparent") - return m(255, 255, 255, 0); - else { - // default to black - res = lookupColors[name] || [0, 0, 0]; - return m(res[0], res[1], res[2]); - } - } - - var lookupColors = { - aqua:[0,255,255], - azure:[240,255,255], - beige:[245,245,220], - black:[0,0,0], - blue:[0,0,255], - brown:[165,42,42], - cyan:[0,255,255], - darkblue:[0,0,139], - darkcyan:[0,139,139], - darkgrey:[169,169,169], - darkgreen:[0,100,0], - darkkhaki:[189,183,107], - darkmagenta:[139,0,139], - darkolivegreen:[85,107,47], - darkorange:[255,140,0], - darkorchid:[153,50,204], - darkred:[139,0,0], - darksalmon:[233,150,122], - darkviolet:[148,0,211], - fuchsia:[255,0,255], - gold:[255,215,0], - green:[0,128,0], - indigo:[75,0,130], - khaki:[240,230,140], - lightblue:[173,216,230], - lightcyan:[224,255,255], - lightgreen:[144,238,144], - lightgrey:[211,211,211], - lightpink:[255,182,193], - lightyellow:[255,255,224], - lime:[0,255,0], - magenta:[255,0,255], - maroon:[128,0,0], - navy:[0,0,128], - olive:[128,128,0], - orange:[255,165,0], - pink:[255,192,203], - purple:[128,0,128], - violet:[128,0,128], - red:[255,0,0], - silver:[192,192,192], - white:[255,255,255], - yellow:[255,255,0] - }; -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.canvas.js b/src/legacy/ui/public/flot-charts/jquery.flot.canvas.js deleted file mode 100644 index 29328d5812127..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.canvas.js +++ /dev/null @@ -1,345 +0,0 @@ -/* Flot plugin for drawing all elements of a plot on the canvas. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Flot normally produces certain elements, like axis labels and the legend, using -HTML elements. This permits greater interactivity and customization, and often -looks better, due to cross-browser canvas text inconsistencies and limitations. - -It can also be desirable to render the plot entirely in canvas, particularly -if the goal is to save it as an image, or if Flot is being used in a context -where the HTML DOM does not exist, as is the case within Node.js. This plugin -switches out Flot's standard drawing operations for canvas-only replacements. - -Currently the plugin supports only axis labels, but it will eventually allow -every element of the plot to be rendered directly to canvas. - -The plugin supports these options: - -{ - canvas: boolean -} - -The "canvas" option controls whether full canvas drawing is enabled, making it -possible to toggle on and off. This is useful when a plot uses HTML text in the -browser, but needs to redraw with canvas text when exporting as an image. - -*/ - -(function($) { - - var options = { - canvas: true - }; - - var render, getTextInfo, addText; - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - function init(plot, classes) { - - var Canvas = classes.Canvas; - - // We only want to replace the functions once; the second time around - // we would just get our new function back. This whole replacing of - // prototype functions is a disaster, and needs to be changed ASAP. - - if (render == null) { - getTextInfo = Canvas.prototype.getTextInfo, - addText = Canvas.prototype.addText, - render = Canvas.prototype.render; - } - - // Finishes rendering the canvas, including overlaid text - - Canvas.prototype.render = function() { - - if (!plot.getOptions().canvas) { - return render.call(this); - } - - var context = this.context, - cache = this._textCache; - - // For each text layer, render elements marked as active - - context.save(); - context.textBaseline = "middle"; - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - var layerCache = cache[layerKey]; - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey], - updateStyles = true; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var info = styleCache[key], - positions = info.positions, - lines = info.lines; - - // Since every element at this level of the cache have the - // same font and fill styles, we can just change them once - // using the values from the first element. - - if (updateStyles) { - context.fillStyle = info.font.color; - context.font = info.font.definition; - updateStyles = false; - } - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - for (var j = 0, line; line = position.lines[j]; j++) { - context.fillText(lines[j].text, line[0], line[1]); - } - } else { - positions.splice(i--, 1); - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - } - } - - context.restore(); - }; - - // Creates (if necessary) and returns a text info object. - // - // When the canvas option is set, the object looks like this: - // - // { - // width: Width of the text's bounding box. - // height: Height of the text's bounding box. - // positions: Array of positions at which this text is drawn. - // lines: [{ - // height: Height of this line. - // widths: Width of this line. - // text: Text on this line. - // }], - // font: { - // definition: Canvas font property string. - // color: Color of the text. - // }, - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // lines: Array of [x, y] coordinates at which to draw the line. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - if (!plot.getOptions().canvas) { - return getTextInfo.call(this, layer, text, font, angle, width); - } - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number - - text = "" + text; - - // If the font is a font-spec object, generate a CSS definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - if (info == null) { - - var context = this.context; - - // If the font was provided as CSS, create a div with those - // classes and examine it to generate a canvas font spec. - - if (typeof font !== "object") { - - var element = $("
 
") - .css("position", "absolute") - .addClass(typeof font === "string" ? font : null) - .appendTo(this.getTextLayer(layer)); - - font = { - lineHeight: element.height(), - style: element.css("font-style"), - variant: element.css("font-variant"), - weight: element.css("font-weight"), - family: element.css("font-family"), - color: element.css("color") - }; - - // Setting line-height to 1, without units, sets it equal - // to the font-size, even if the font-size is abstract, - // like 'smaller'. This enables us to read the real size - // via the element's height, working around browsers that - // return the literal 'smaller' value. - - font.size = element.css("line-height", 1).height(); - - element.remove(); - } - - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; - - // Create a new info object, initializing the dimensions to - // zero so we can count them up line-by-line. - - info = styleCache[text] = { - width: 0, - height: 0, - positions: [], - lines: [], - font: { - definition: textStyle, - color: font.color - } - }; - - context.save(); - context.font = textStyle; - - // Canvas can't handle multi-line strings; break on various - // newlines, including HTML brs, to build a list of lines. - // Note that we could split directly on regexps, but IE < 9 is - // broken; revisit when we drop IE 7/8 support. - - var lines = (text + "").replace(/
|\r\n|\r/g, "\n").split("\n"); - - for (var i = 0; i < lines.length; ++i) { - - var lineText = lines[i], - measured = context.measureText(lineText); - - info.width = Math.max(measured.width, info.width); - info.height += font.lineHeight; - - info.lines.push({ - text: lineText, - width: measured.width, - height: font.lineHeight - }); - } - - context.restore(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - if (!plot.getOptions().canvas) { - return addText.call(this, layer, x, y, text, font, angle, width, halign, valign); - } - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions, - lines = info.lines; - - // Text is drawn with baseline 'middle', which we need to account - // for by adding half a line's height to the y position. - - y += info.height / lines.length / 2; - - // Tweak the initial y-position to match vertical alignment - - if (valign == "middle") { - y = Math.round(y - info.height / 2); - } else if (valign == "bottom") { - y = Math.round(y - info.height); - } else { - y = Math.round(y); - } - - // FIXME: LEGACY BROWSER FIX - // AFFECTS: Opera < 12.00 - - // Offset the y coordinate, since Opera is off pretty - // consistently compared to the other browsers. - - if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { - y -= 2; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - position = { - active: true, - lines: [], - x: x, - y: y - }; - - positions.push(position); - - // Fill in the x & y positions of each line, adjusting them - // individually for horizontal alignment. - - for (var i = 0, line; line = lines[i]; i++) { - if (halign == "center") { - position.lines.push([Math.round(x - line.width / 2), y]); - } else if (halign == "right") { - position.lines.push([Math.round(x - line.width), y]); - } else { - position.lines.push([Math.round(x), y]); - } - y += line.height; - } - }; - } - - $.plot.plugins.push({ - init: init, - options: options, - name: "canvas", - version: "1.0" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.categories.js b/src/legacy/ui/public/flot-charts/jquery.flot.categories.js deleted file mode 100644 index 2f9b257971499..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.categories.js +++ /dev/null @@ -1,190 +0,0 @@ -/* Flot plugin for plotting textual data or categories. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin -allows you to plot such a dataset directly. - -To enable it, you must specify mode: "categories" on the axis with the textual -labels, e.g. - - $.plot("#placeholder", data, { xaxis: { mode: "categories" } }); - -By default, the labels are ordered as they are met in the data series. If you -need a different ordering, you can specify "categories" on the axis options -and list the categories there: - - xaxis: { - mode: "categories", - categories: ["February", "March", "April"] - } - -If you need to customize the distances between the categories, you can specify -"categories" as an object mapping labels to values - - xaxis: { - mode: "categories", - categories: { "February": 1, "March": 3, "April": 4 } - } - -If you don't specify all categories, the remaining categories will be numbered -from the max value plus 1 (with a spacing of 1 between each). - -Internally, the plugin works by transforming the input data through an auto- -generated mapping where the first category becomes 0, the second 1, etc. -Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this -is visible in hover and click events that return numbers rather than the -category labels). The plugin also overrides the tick generator to spit out the -categories as ticks instead of the values. - -If you need to map a value back to its label, the mapping is always accessible -as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories. - -*/ - -(function ($) { - var options = { - xaxis: { - categories: null - }, - yaxis: { - categories: null - } - }; - - function processRawData(plot, series, data, datapoints) { - // if categories are enabled, we need to disable - // auto-transformation to numbers so the strings are intact - // for later processing - - var xCategories = series.xaxis.options.mode == "categories", - yCategories = series.yaxis.options.mode == "categories"; - - if (!(xCategories || yCategories)) - return; - - var format = datapoints.format; - - if (!format) { - // FIXME: auto-detection should really not be defined here - var s = series; - format = []; - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - datapoints.format = format; - } - - for (var m = 0; m < format.length; ++m) { - if (format[m].x && xCategories) - format[m].number = false; - - if (format[m].y && yCategories) - format[m].number = false; - } - } - - function getNextIndex(categories) { - var index = -1; - - for (var v in categories) - if (categories[v] > index) - index = categories[v]; - - return index + 1; - } - - function categoriesTickGenerator(axis) { - var res = []; - for (var label in axis.categories) { - var v = axis.categories[label]; - if (v >= axis.min && v <= axis.max) - res.push([v, label]); - } - - res.sort(function (a, b) { return a[0] - b[0]; }); - - return res; - } - - function setupCategoriesForAxis(series, axis, datapoints) { - if (series[axis].options.mode != "categories") - return; - - if (!series[axis].categories) { - // parse options - var c = {}, o = series[axis].options.categories || {}; - if ($.isArray(o)) { - for (var i = 0; i < o.length; ++i) - c[o[i]] = i; - } - else { - for (var v in o) - c[v] = o[v]; - } - - series[axis].categories = c; - } - - // fix ticks - if (!series[axis].options.ticks) - series[axis].options.ticks = categoriesTickGenerator; - - transformPointsOnAxis(datapoints, axis, series[axis].categories); - } - - function transformPointsOnAxis(datapoints, axis, categories) { - // go through the points, transforming them - var points = datapoints.points, - ps = datapoints.pointsize, - format = datapoints.format, - formatColumn = axis.charAt(0), - index = getNextIndex(categories); - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - - for (var m = 0; m < ps; ++m) { - var val = points[i + m]; - - if (val == null || !format[m][formatColumn]) - continue; - - if (!(val in categories)) { - categories[val] = index; - ++index; - } - - points[i + m] = categories[val]; - } - } - } - - function processDatapoints(plot, series, datapoints) { - setupCategoriesForAxis(series, "xaxis", datapoints); - setupCategoriesForAxis(series, "yaxis", datapoints); - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.processDatapoints.push(processDatapoints); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'categories', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.crosshair.js b/src/legacy/ui/public/flot-charts/jquery.flot.crosshair.js deleted file mode 100644 index 5111695e3d12c..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.crosshair.js +++ /dev/null @@ -1,176 +0,0 @@ -/* Flot plugin for showing crosshairs when the mouse hovers over the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - crosshair: { - mode: null or "x" or "y" or "xy" - color: color - lineWidth: number - } - -Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical -crosshair that lets you trace the values on the x axis, "y" enables a -horizontal crosshair and "xy" enables them both. "color" is the color of the -crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of -the drawn lines (default is 1). - -The plugin also adds four public methods: - - - setCrosshair( pos ) - - Set the position of the crosshair. Note that this is cleared if the user - moves the mouse. "pos" is in coordinates of the plot and should be on the - form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple - axes), which is coincidentally the same format as what you get from a - "plothover" event. If "pos" is null, the crosshair is cleared. - - - clearCrosshair() - - Clear the crosshair. - - - lockCrosshair(pos) - - Cause the crosshair to lock to the current location, no longer updating if - the user moves the mouse. Optionally supply a position (passed on to - setCrosshair()) to move it to. - - Example usage: - - var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; - $("#graph").bind( "plothover", function ( evt, position, item ) { - if ( item ) { - // Lock the crosshair to the data point being hovered - myFlot.lockCrosshair({ - x: item.datapoint[ 0 ], - y: item.datapoint[ 1 ] - }); - } else { - // Return normal crosshair operation - myFlot.unlockCrosshair(); - } - }); - - - unlockCrosshair() - - Free the crosshair to move again after locking it. -*/ - -(function ($) { - var options = { - crosshair: { - mode: null, // one of null, "x", "y" or "xy", - color: "rgba(170, 0, 0, 0.80)", - lineWidth: 1 - } - }; - - function init(plot) { - // position of crosshair in pixels - var crosshair = { x: -1, y: -1, locked: false }; - - plot.setCrosshair = function setCrosshair(pos) { - if (!pos) - crosshair.x = -1; - else { - var o = plot.p2c(pos); - crosshair.x = Math.max(0, Math.min(o.left, plot.width())); - crosshair.y = Math.max(0, Math.min(o.top, plot.height())); - } - - plot.triggerRedrawOverlay(); - }; - - plot.clearCrosshair = plot.setCrosshair; // passes null for pos - - plot.lockCrosshair = function lockCrosshair(pos) { - if (pos) - plot.setCrosshair(pos); - crosshair.locked = true; - }; - - plot.unlockCrosshair = function unlockCrosshair() { - crosshair.locked = false; - }; - - function onMouseOut(e) { - if (crosshair.locked) - return; - - if (crosshair.x != -1) { - crosshair.x = -1; - plot.triggerRedrawOverlay(); - } - } - - function onMouseMove(e) { - if (crosshair.locked) - return; - - if (plot.getSelection && plot.getSelection()) { - crosshair.x = -1; // hide the crosshair while selecting - return; - } - - var offset = plot.offset(); - crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); - crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); - plot.triggerRedrawOverlay(); - } - - plot.hooks.bindEvents.push(function (plot, eventHolder) { - if (!plot.getOptions().crosshair.mode) - return; - - eventHolder.mouseout(onMouseOut); - eventHolder.mousemove(onMouseMove); - }); - - plot.hooks.drawOverlay.push(function (plot, ctx) { - var c = plot.getOptions().crosshair; - if (!c.mode) - return; - - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - if (crosshair.x != -1) { - var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; - - ctx.strokeStyle = c.color; - ctx.lineWidth = c.lineWidth; - ctx.lineJoin = "round"; - - ctx.beginPath(); - if (c.mode.indexOf("x") != -1) { - var drawX = Math.floor(crosshair.x) + adj; - ctx.moveTo(drawX, 0); - ctx.lineTo(drawX, plot.height()); - } - if (c.mode.indexOf("y") != -1) { - var drawY = Math.floor(crosshair.y) + adj; - ctx.moveTo(0, drawY); - ctx.lineTo(plot.width(), drawY); - } - ctx.stroke(); - } - ctx.restore(); - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mouseout", onMouseOut); - eventHolder.unbind("mousemove", onMouseMove); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'crosshair', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.errorbars.js b/src/legacy/ui/public/flot-charts/jquery.flot.errorbars.js deleted file mode 100644 index 655036e0db846..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.errorbars.js +++ /dev/null @@ -1,353 +0,0 @@ -/* Flot plugin for plotting error bars. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Error bars are used to show standard deviation and other statistical -properties in a plot. - -* Created by Rui Pereira - rui (dot) pereira (at) gmail (dot) com - -This plugin allows you to plot error-bars over points. Set "errorbars" inside -the points series to the axis name over which there will be error values in -your data array (*even* if you do not intend to plot them later, by setting -"show: null" on xerr/yerr). - -The plugin supports these options: - - series: { - points: { - errorbars: "x" or "y" or "xy", - xerr: { - show: null/false or true, - asymmetric: null/false or true, - upperCap: null or "-" or function, - lowerCap: null or "-" or function, - color: null or color, - radius: null or number - }, - yerr: { same options as xerr } - } - } - -Each data point array is expected to be of the type: - - "x" [ x, y, xerr ] - "y" [ x, y, yerr ] - "xy" [ x, y, xerr, yerr ] - -Where xerr becomes xerr_lower,xerr_upper for the asymmetric error case, and -equivalently for yerr. E.g., a datapoint for the "xy" case with symmetric -error-bars on X and asymmetric on Y would be: - - [ x, y, xerr, yerr_lower, yerr_upper ] - -By default no end caps are drawn. Setting upperCap and/or lowerCap to "-" will -draw a small cap perpendicular to the error bar. They can also be set to a -user-defined drawing function, with (ctx, x, y, radius) as parameters, as e.g.: - - function drawSemiCircle( ctx, x, y, radius ) { - ctx.beginPath(); - ctx.arc( x, y, radius, 0, Math.PI, false ); - ctx.moveTo( x - radius, y ); - ctx.lineTo( x + radius, y ); - ctx.stroke(); - } - -Color and radius both default to the same ones of the points series if not -set. The independent radius parameter on xerr/yerr is useful for the case when -we may want to add error-bars to a line, without showing the interconnecting -points (with radius: 0), and still showing end caps on the error-bars. -shadowSize and lineWidth are derived as well from the points series. - -*/ - -(function ($) { - var options = { - series: { - points: { - errorbars: null, //should be 'x', 'y' or 'xy' - xerr: { err: 'x', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null}, - yerr: { err: 'y', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null} - } - } - }; - - function processRawData(plot, series, data, datapoints){ - if (!series.points.errorbars) - return; - - // x,y values - var format = [ - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ]; - - var errors = series.points.errorbars; - // error bars - first X then Y - if (errors == 'x' || errors == 'xy') { - // lower / upper error - if (series.points.xerr.asymmetric) { - format.push({ x: true, number: true, required: true }); - format.push({ x: true, number: true, required: true }); - } else - format.push({ x: true, number: true, required: true }); - } - if (errors == 'y' || errors == 'xy') { - // lower / upper error - if (series.points.yerr.asymmetric) { - format.push({ y: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - } else - format.push({ y: true, number: true, required: true }); - } - datapoints.format = format; - } - - function parseErrors(series, i){ - - var points = series.datapoints.points; - - // read errors from points array - var exl = null, - exu = null, - eyl = null, - eyu = null; - var xerr = series.points.xerr, - yerr = series.points.yerr; - - var eb = series.points.errorbars; - // error bars - first X - if (eb == 'x' || eb == 'xy') { - if (xerr.asymmetric) { - exl = points[i + 2]; - exu = points[i + 3]; - if (eb == 'xy') - if (yerr.asymmetric){ - eyl = points[i + 4]; - eyu = points[i + 5]; - } else eyl = points[i + 4]; - } else { - exl = points[i + 2]; - if (eb == 'xy') - if (yerr.asymmetric) { - eyl = points[i + 3]; - eyu = points[i + 4]; - } else eyl = points[i + 3]; - } - // only Y - } else if (eb == 'y') - if (yerr.asymmetric) { - eyl = points[i + 2]; - eyu = points[i + 3]; - } else eyl = points[i + 2]; - - // symmetric errors? - if (exu == null) exu = exl; - if (eyu == null) eyu = eyl; - - var errRanges = [exl, exu, eyl, eyu]; - // nullify if not showing - if (!xerr.show){ - errRanges[0] = null; - errRanges[1] = null; - } - if (!yerr.show){ - errRanges[2] = null; - errRanges[3] = null; - } - return errRanges; - } - - function drawSeriesErrors(plot, ctx, s){ - - var points = s.datapoints.points, - ps = s.datapoints.pointsize, - ax = [s.xaxis, s.yaxis], - radius = s.points.radius, - err = [s.points.xerr, s.points.yerr]; - - //sanity check, in case some inverted axis hack is applied to flot - var invertX = false; - if (ax[0].p2c(ax[0].max) < ax[0].p2c(ax[0].min)) { - invertX = true; - var tmp = err[0].lowerCap; - err[0].lowerCap = err[0].upperCap; - err[0].upperCap = tmp; - } - - var invertY = false; - if (ax[1].p2c(ax[1].min) < ax[1].p2c(ax[1].max)) { - invertY = true; - var tmp = err[1].lowerCap; - err[1].lowerCap = err[1].upperCap; - err[1].upperCap = tmp; - } - - for (var i = 0; i < s.datapoints.points.length; i += ps) { - - //parse - var errRanges = parseErrors(s, i); - - //cycle xerr & yerr - for (var e = 0; e < err.length; e++){ - - var minmax = [ax[e].min, ax[e].max]; - - //draw this error? - if (errRanges[e * err.length]){ - - //data coordinates - var x = points[i], - y = points[i + 1]; - - //errorbar ranges - var upper = [x, y][e] + errRanges[e * err.length + 1], - lower = [x, y][e] - errRanges[e * err.length]; - - //points outside of the canvas - if (err[e].err == 'x') - if (y > ax[1].max || y < ax[1].min || upper < ax[0].min || lower > ax[0].max) - continue; - if (err[e].err == 'y') - if (x > ax[0].max || x < ax[0].min || upper < ax[1].min || lower > ax[1].max) - continue; - - // prevent errorbars getting out of the canvas - var drawUpper = true, - drawLower = true; - - if (upper > minmax[1]) { - drawUpper = false; - upper = minmax[1]; - } - if (lower < minmax[0]) { - drawLower = false; - lower = minmax[0]; - } - - //sanity check, in case some inverted axis hack is applied to flot - if ((err[e].err == 'x' && invertX) || (err[e].err == 'y' && invertY)) { - //swap coordinates - var tmp = lower; - lower = upper; - upper = tmp; - tmp = drawLower; - drawLower = drawUpper; - drawUpper = tmp; - tmp = minmax[0]; - minmax[0] = minmax[1]; - minmax[1] = tmp; - } - - // convert to pixels - x = ax[0].p2c(x), - y = ax[1].p2c(y), - upper = ax[e].p2c(upper); - lower = ax[e].p2c(lower); - minmax[0] = ax[e].p2c(minmax[0]); - minmax[1] = ax[e].p2c(minmax[1]); - - //same style as points by default - var lw = err[e].lineWidth ? err[e].lineWidth : s.points.lineWidth, - sw = s.points.shadowSize != null ? s.points.shadowSize : s.shadowSize; - - //shadow as for points - if (lw > 0 && sw > 0) { - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w + w/2, minmax); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w/2, minmax); - } - - ctx.strokeStyle = err[e].color? err[e].color: s.color; - ctx.lineWidth = lw; - //draw it - drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, 0, minmax); - } - } - } - } - - function drawError(ctx,err,x,y,upper,lower,drawUpper,drawLower,radius,offset,minmax){ - - //shadow offset - y += offset; - upper += offset; - lower += offset; - - // error bar - avoid plotting over circles - if (err.err == 'x'){ - if (upper > x + radius) drawPath(ctx, [[upper,y],[Math.max(x + radius,minmax[0]),y]]); - else drawUpper = false; - if (lower < x - radius) drawPath(ctx, [[Math.min(x - radius,minmax[1]),y],[lower,y]] ); - else drawLower = false; - } - else { - if (upper < y - radius) drawPath(ctx, [[x,upper],[x,Math.min(y - radius,minmax[0])]] ); - else drawUpper = false; - if (lower > y + radius) drawPath(ctx, [[x,Math.max(y + radius,minmax[1])],[x,lower]] ); - else drawLower = false; - } - - //internal radius value in errorbar, allows to plot radius 0 points and still keep proper sized caps - //this is a way to get errorbars on lines without visible connecting dots - radius = err.radius != null? err.radius: radius; - - // upper cap - if (drawUpper) { - if (err.upperCap == '-'){ - if (err.err=='x') drawPath(ctx, [[upper,y - radius],[upper,y + radius]] ); - else drawPath(ctx, [[x - radius,upper],[x + radius,upper]] ); - } else if ($.isFunction(err.upperCap)){ - if (err.err=='x') err.upperCap(ctx, upper, y, radius); - else err.upperCap(ctx, x, upper, radius); - } - } - // lower cap - if (drawLower) { - if (err.lowerCap == '-'){ - if (err.err=='x') drawPath(ctx, [[lower,y - radius],[lower,y + radius]] ); - else drawPath(ctx, [[x - radius,lower],[x + radius,lower]] ); - } else if ($.isFunction(err.lowerCap)){ - if (err.err=='x') err.lowerCap(ctx, lower, y, radius); - else err.lowerCap(ctx, x, lower, radius); - } - } - } - - function drawPath(ctx, pts){ - ctx.beginPath(); - ctx.moveTo(pts[0][0], pts[0][1]); - for (var p=1; p < pts.length; p++) - ctx.lineTo(pts[p][0], pts[p][1]); - ctx.stroke(); - } - - function draw(plot, ctx){ - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - $.each(plot.getData(), function (i, s) { - if (s.points.errorbars && (s.points.xerr.show || s.points.yerr.show)) - drawSeriesErrors(plot, ctx, s); - }); - ctx.restore(); - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.draw.push(draw); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'errorbars', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.fillbetween.js b/src/legacy/ui/public/flot-charts/jquery.flot.fillbetween.js deleted file mode 100644 index 18b15d26db8c9..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.fillbetween.js +++ /dev/null @@ -1,226 +0,0 @@ -/* Flot plugin for computing bottoms for filled line and bar charts. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The case: you've got two series that you want to fill the area between. In Flot -terms, you need to use one as the fill bottom of the other. You can specify the -bottom of each data point as the third coordinate manually, or you can use this -plugin to compute it for you. - -In order to name the other series, you need to give it an id, like this: - - var dataset = [ - { data: [ ... ], id: "foo" } , // use default bottom - { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom - ]; - - $.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); - -As a convenience, if the id given is a number that doesn't appear as an id in -the series, it is interpreted as the index in the array instead (so fillBetween: -0 can also mean the first series). - -Internally, the plugin modifies the datapoints in each series. For line series, -extra data points might be inserted through interpolation. Note that at points -where the bottom line is not defined (due to a null point or start/end of line), -the current line will show a gap too. The algorithm comes from the -jquery.flot.stack.js plugin, possibly some code could be shared. - -*/ - -(function ( $ ) { - - var options = { - series: { - fillBetween: null // or number - } - }; - - function init( plot ) { - - function findBottomSeries( s, allseries ) { - - var i; - - for ( i = 0; i < allseries.length; ++i ) { - if ( allseries[ i ].id === s.fillBetween ) { - return allseries[ i ]; - } - } - - if ( typeof s.fillBetween === "number" ) { - if ( s.fillBetween < 0 || s.fillBetween >= allseries.length ) { - return null; - } - return allseries[ s.fillBetween ]; - } - - return null; - } - - function computeFillBottoms( plot, s, datapoints ) { - - if ( s.fillBetween == null ) { - return; - } - - var other = findBottomSeries( s, plot.getData() ); - - if ( !other ) { - return; - } - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - withbottom = ps > 2 && datapoints.format[2].y, - withsteps = withlines && s.lines.steps, - fromgap = true, - i = 0, - j = 0, - l, m; - - while ( true ) { - - if ( i >= points.length ) { - break; - } - - l = newpoints.length; - - if ( points[ i ] == null ) { - - // copy gaps - - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - - i += ps; - - } else if ( j >= otherpoints.length ) { - - // for lines, we can't use the rest of the points - - if ( !withlines ) { - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - } - - i += ps; - - } else if ( otherpoints[ j ] == null ) { - - // oops, got a gap - - for ( m = 0; m < ps; ++m ) { - newpoints.push( null ); - } - - fromgap = true; - j += otherps; - - } else { - - // cases where we actually got two points - - px = points[ i ]; - py = points[ i + 1 ]; - qx = otherpoints[ j ]; - qy = otherpoints[ j + 1 ]; - bottom = 0; - - if ( px === qx ) { - - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - - //newpoints[ l + 1 ] += qy; - bottom = qy; - - i += ps; - j += otherps; - - } else if ( px > qx ) { - - // we got past point below, might need to - // insert interpolated extra point - - if ( withlines && i > 0 && points[ i - ps ] != null ) { - intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px ); - newpoints.push( qx ); - newpoints.push( intery ); - for ( m = 2; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - bottom = qy; - } - - j += otherps; - - } else { // px < qx - - // if we come from a gap, we just skip this point - - if ( fromgap && withlines ) { - i += ps; - continue; - } - - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - - // we might be able to interpolate a point below, - // this can give us a better y - - if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) { - bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx ); - } - - //newpoints[l + 1] += bottom; - - i += ps; - } - - fromgap = false; - - if ( l !== newpoints.length && withbottom ) { - newpoints[ l + 2 ] = bottom; - } - } - - // maintain the line steps invariant - - if ( withsteps && l !== newpoints.length && l > 0 && - newpoints[ l ] !== null && - newpoints[ l ] !== newpoints[ l - ps ] && - newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) { - for (m = 0; m < ps; ++m) { - newpoints[ l + ps + m ] = newpoints[ l + m ]; - } - newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push( computeFillBottoms ); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: "fillbetween", - version: "1.0" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.image.js b/src/legacy/ui/public/flot-charts/jquery.flot.image.js deleted file mode 100644 index 178f0e69069ef..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.image.js +++ /dev/null @@ -1,241 +0,0 @@ -/* Flot plugin for plotting images. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The data syntax is [ [ image, x1, y1, x2, y2 ], ... ] where (x1, y1) and -(x2, y2) are where you intend the two opposite corners of the image to end up -in the plot. Image must be a fully loaded JavaScript image (you can make one -with new Image()). If the image is not complete, it's skipped when plotting. - -There are two helpers included for retrieving images. The easiest work the way -that you put in URLs instead of images in the data, like this: - - [ "myimage.png", 0, 0, 10, 10 ] - -Then call $.plot.image.loadData( data, options, callback ) where data and -options are the same as you pass in to $.plot. This loads the images, replaces -the URLs in the data with the corresponding images and calls "callback" when -all images are loaded (or failed loading). In the callback, you can then call -$.plot with the data set. See the included example. - -A more low-level helper, $.plot.image.load(urls, callback) is also included. -Given a list of URLs, it calls callback with an object mapping from URL to -Image object when all images are loaded or have failed loading. - -The plugin supports these options: - - series: { - images: { - show: boolean - anchor: "corner" or "center" - alpha: [ 0, 1 ] - } - } - -They can be specified for a specific series: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - images: { ... } - ]) - -Note that because the data format is different from usual data points, you -can't use images with anything else in a specific data series. - -Setting "anchor" to "center" causes the pixels in the image to be anchored at -the corner pixel centers inside of at the pixel corners, effectively letting -half a pixel stick out to each side in the plot. - -A possible future direction could be support for tiling for large images (like -Google Maps). - -*/ - -(function ($) { - var options = { - series: { - images: { - show: false, - alpha: 1, - anchor: "corner" // or "center" - } - } - }; - - $.plot.image = {}; - - $.plot.image.loadDataImages = function (series, options, callback) { - var urls = [], points = []; - - var defaultShow = options.series.images.show; - - $.each(series, function (i, s) { - if (!(defaultShow || s.images.show)) - return; - - if (s.data) - s = s.data; - - $.each(s, function (i, p) { - if (typeof p[0] == "string") { - urls.push(p[0]); - points.push(p); - } - }); - }); - - $.plot.image.load(urls, function (loadedImages) { - $.each(points, function (i, p) { - var url = p[0]; - if (loadedImages[url]) - p[0] = loadedImages[url]; - }); - - callback(); - }); - } - - $.plot.image.load = function (urls, callback) { - var missing = urls.length, loaded = {}; - if (missing == 0) - callback({}); - - $.each(urls, function (i, url) { - var handler = function () { - --missing; - - loaded[url] = this; - - if (missing == 0) - callback(loaded); - }; - - $('').load(handler).error(handler).attr('src', url); - }); - }; - - function drawSeries(plot, ctx, series) { - var plotOffset = plot.getPlotOffset(); - - if (!series.images || !series.images.show) - return; - - var points = series.datapoints.points, - ps = series.datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var img = points[i], - x1 = points[i + 1], y1 = points[i + 2], - x2 = points[i + 3], y2 = points[i + 4], - xaxis = series.xaxis, yaxis = series.yaxis, - tmp; - - // actually we should check img.complete, but it - // appears to be a somewhat unreliable indicator in - // IE6 (false even after load event) - if (!img || img.width <= 0 || img.height <= 0) - continue; - - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - // if the anchor is at the center of the pixel, expand the - // image by 1/2 pixel in each direction - if (series.images.anchor == "center") { - tmp = 0.5 * (x2-x1) / (img.width - 1); - x1 -= tmp; - x2 += tmp; - tmp = 0.5 * (y2-y1) / (img.height - 1); - y1 -= tmp; - y2 += tmp; - } - - // clip - if (x1 == x2 || y1 == y2 || - x1 >= xaxis.max || x2 <= xaxis.min || - y1 >= yaxis.max || y2 <= yaxis.min) - continue; - - var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; - if (x1 < xaxis.min) { - sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); - x1 = xaxis.min; - } - - if (x2 > xaxis.max) { - sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); - x2 = xaxis.max; - } - - if (y1 < yaxis.min) { - sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); - y1 = yaxis.min; - } - - if (y2 > yaxis.max) { - sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); - y2 = yaxis.max; - } - - x1 = xaxis.p2c(x1); - x2 = xaxis.p2c(x2); - y1 = yaxis.p2c(y1); - y2 = yaxis.p2c(y2); - - // the transformation may have swapped us - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - tmp = ctx.globalAlpha; - ctx.globalAlpha *= series.images.alpha; - ctx.drawImage(img, - sx1, sy1, sx2 - sx1, sy2 - sy1, - x1 + plotOffset.left, y1 + plotOffset.top, - x2 - x1, y2 - y1); - ctx.globalAlpha = tmp; - } - } - - function processRawData(plot, series, data, datapoints) { - if (!series.images.show) - return; - - // format is Image, x1, y1, x2, y2 (opposite corners) - datapoints.format = [ - { required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ]; - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.drawSeries.push(drawSeries); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'image', - version: '1.1' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.js b/src/legacy/ui/public/flot-charts/jquery.flot.js deleted file mode 100644 index 43db1cc3d93db..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.js +++ /dev/null @@ -1,3168 +0,0 @@ -/* JavaScript plotting library for jQuery, version 0.8.3. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -*/ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ -(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); - -// the actual Flot code -(function($) { - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM - // operation produces the same effect as detach, i.e. removing the element - // without touching its jQuery data. - - // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. - - if (!$.fn.detach) { - $.fn.detach = function() { - return this.each(function() { - if (this.parentNode) { - this.parentNode.removeChild( this ); - } - }); - }; - } - - /////////////////////////////////////////////////////////////////////////// - // The Canvas object is a wrapper around an HTML5 tag. - // - // @constructor - // @param {string} cls List of classes to apply to the canvas. - // @param {element} container Element onto which to append the canvas. - // - // Requiring a container is a little iffy, but unfortunately canvas - // operations don't work unless the canvas is attached to the DOM. - - function Canvas(cls, container) { - - var element = container.children("." + cls)[0]; - - if (element == null) { - - element = document.createElement("canvas"); - element.className = cls; - - $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) - .appendTo(container); - - // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas - - if (!element.getContext) { - if (window.G_vmlCanvasManager) { - element = window.G_vmlCanvasManager.initElement(element); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - } - - this.element = element; - - var context = this.context = element.getContext("2d"); - - // Determine the screen's ratio of physical to device-independent - // pixels. This is the ratio between the canvas width that the browser - // advertises and the number of pixels actually present in that space. - - // The iPhone 4, for example, has a device-independent width of 320px, - // but its screen is actually 640px wide. It therefore has a pixel - // ratio of 2, while most normal devices have a ratio of 1. - - var devicePixelRatio = window.devicePixelRatio || 1, - backingStoreRatio = - context.webkitBackingStorePixelRatio || - context.mozBackingStorePixelRatio || - context.msBackingStorePixelRatio || - context.oBackingStorePixelRatio || - context.backingStorePixelRatio || 1; - - this.pixelRatio = devicePixelRatio / backingStoreRatio; - - // Size the canvas to match the internal dimensions of its container - - this.resize(container.width(), container.height()); - - // Collection of HTML div layers for text overlaid onto the canvas - - this.textContainer = null; - this.text = {}; - - // Cache of text fragments and metrics, so we can avoid expensively - // re-calculating them when the plot is re-rendered in a loop. - - this._textCache = {}; - } - - // Resizes the canvas to the given dimensions. - // - // @param {number} width New width of the canvas, in pixels. - // @param {number} width New height of the canvas, in pixels. - - Canvas.prototype.resize = function(width, height) { - - if (width <= 0 || height <= 0) { - throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); - } - - var element = this.element, - context = this.context, - pixelRatio = this.pixelRatio; - - // Resize the canvas, increasing its density based on the display's - // pixel ratio; basically giving it more pixels without increasing the - // size of its element, to take advantage of the fact that retina - // displays have that many more pixels in the same advertised space. - - // Resizing should reset the state (excanvas seems to be buggy though) - - if (this.width != width) { - element.width = width * pixelRatio; - element.style.width = width + "px"; - this.width = width; - } - - if (this.height != height) { - element.height = height * pixelRatio; - element.style.height = height + "px"; - this.height = height; - } - - // Save the context, so we can reset in case we get replotted. The - // restore ensure that we're really back at the initial state, and - // should be safe even if we haven't saved the initial state yet. - - context.restore(); - context.save(); - - // Scale the coordinate space to match the display density; so even though we - // may have twice as many pixels, we still want lines and other drawing to - // appear at the same size; the extra pixels will just make them crisper. - - context.scale(pixelRatio, pixelRatio); - }; - - // Clears the entire canvas area, not including any overlaid HTML text - - Canvas.prototype.clear = function() { - this.context.clearRect(0, 0, this.width, this.height); - }; - - // Finishes rendering the canvas, including managing the text overlay. - - Canvas.prototype.render = function() { - - var cache = this._textCache; - - // For each text layer, add elements marked as active that haven't - // already been rendered, and remove those that are no longer active. - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - - var layer = this.getTextLayer(layerKey), - layerCache = cache[layerKey]; - - layer.hide(); - - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var positions = styleCache[key].positions; - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - if (!position.rendered) { - layer.append(position.element); - position.rendered = true; - } - } else { - positions.splice(i--, 1); - if (position.rendered) { - position.element.detach(); - } - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - - layer.show(); - } - } - }; - - // Creates (if necessary) and returns the text overlay container. - // - // @param {string} classes String of space-separated CSS classes used to - // uniquely identify the text layer. - // @return {object} The jQuery-wrapped text-layer div. - - Canvas.prototype.getTextLayer = function(classes) { - - var layer = this.text[classes]; - - // Create the text layer if it doesn't exist - - if (layer == null) { - - // Create the text layer container, if it doesn't exist - - if (this.textContainer == null) { - this.textContainer = $("
") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0, - 'font-size': "smaller", - color: "#545454" - }) - .insertAfter(this.element); - } - - layer = this.text[classes] = $("
") - .addClass(classes) - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }) - .appendTo(this.textContainer); - } - - return layer; - }; - - // Creates (if necessary) and returns a text info object. - // - // The object looks like this: - // - // { - // width: Width of the text's wrapper div. - // height: Height of the text's wrapper div. - // element: The jQuery-wrapped HTML div containing the text. - // positions: Array of positions at which this text is drawn. - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // rendered: Flag indicating whether the text is currently visible. - // element: The jQuery-wrapped HTML div containing the text. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - // - // Each position after the first receives a clone of the original element. - // - // The idea is that that the width, height, and general 'identity' of the - // text is constant no matter where it is placed; the placements are a - // secondary property. - // - // Canvas maintains a cache of recently-used text info objects; getTextInfo - // either returns the cached element or creates a new entry. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {string} text Text string to retrieve info for. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @return {object} a text info object. - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number or such - - text = "" + text; - - // If the font is a font-spec object, generate a CSS font definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - // If we can't find a matching element in our cache, create a new one - - if (info == null) { - - var element = $("
").html(text) - .css({ - position: "absolute", - 'max-width': width, - top: -9999 - }) - .appendTo(this.getTextLayer(layer)); - - if (typeof font === "object") { - element.css({ - font: textStyle, - color: font.color - }); - } else if (typeof font === "string") { - element.addClass(font); - } - - info = styleCache[text] = { - width: element.outerWidth(true), - height: element.outerHeight(true), - element: element, - positions: [] - }; - - element.detach(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - // - // The text isn't drawn immediately; it is marked as rendering, which will - // result in its addition to the canvas on the next render pass. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number} x X coordinate at which to draw the text. - // @param {number} y Y coordinate at which to draw the text. - // @param {string} text Text string to draw. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @param {string=} halign Horizontal alignment of the text; either "left", - // "center" or "right". - // @param {string=} valign Vertical alignment of the text; either "top", - // "middle" or "bottom". - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions; - - // Tweak the div's position to match the text's alignment - - if (halign == "center") { - x -= info.width / 2; - } else if (halign == "right") { - x -= info.width; - } - - if (valign == "middle") { - y -= info.height / 2; - } else if (valign == "bottom") { - y -= info.height; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - // For the very first position we'll re-use the original element, - // while for subsequent ones we'll clone it. - - position = { - active: true, - rendered: false, - element: positions.length ? info.element.clone() : info.element, - x: x, - y: y - }; - - positions.push(position); - - // Move the element to its final position within the container - - position.element.css({ - top: Math.round(y), - left: Math.round(x), - 'text-align': halign // In case the text wraps - }); - }; - - // Removes one or more text strings from the canvas text overlay. - // - // If no parameters are given, all text within the layer is removed. - // - // Note that the text is not immediately removed; it is simply marked as - // inactive, which will result in its removal on the next render pass. - // This avoids the performance penalty for 'clear and redraw' behavior, - // where we potentially get rid of all text on a layer, but will likely - // add back most or all of it later, as when redrawing axes, for example. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number=} x X coordinate of the text. - // @param {number=} y Y coordinate of the text. - // @param {string=} text Text string to remove. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which the text is rotated, in degrees. - // Angle is currently unused, it will be implemented in the future. - - Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { - if (text == null) { - var layerCache = this._textCache[layer]; - if (layerCache != null) { - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - var positions = styleCache[key].positions; - for (var i = 0, position; position = positions[i]; i++) { - position.active = false; - } - } - } - } - } - } - } else { - var positions = this.getTextInfo(layer, text, font, angle).positions; - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = false; - } - } - } - }; - - /////////////////////////////////////////////////////////////////////////// - // The top-level container for the entire plot. - - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of columns in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85, // set to 0 to avoid background - sorted: null // default to no legend sorting - }, - xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null // number or [number, "unit"] - }, - yaxis: { - autoscaleMargin: 0.02, - position: "left" // or "right" - }, - xaxes: [], - yaxes: [], - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff", - symbol: "circle" // or callback - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - // Omit 'zero', so we can later default its value to - // match that of the 'fill' option. - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // "left", "right", or "center" - horizontal: false, - zero: true - }, - shadowSize: 3, - highlightColor: null - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - borderColor: null, // set if different from the grid color - tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" - margin: 0, // distance from the canvas edge to the grid - labelMargin: 5, // in pixels - axisMargin: 8, // in pixels - borderWidth: 2, // in pixels - minBorderMargin: null, // in pixels, null means taken from points radius - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - interaction: { - redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow - }, - hooks: {} - }, - surface = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - processOffset: [], - drawBackground: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return surface.element; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function () { return series; }; - plot.getAxes = function () { - var res = {}, i; - $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; - }); - return res; - }; - plot.getXAxes = function () { return xaxes; }; - plot.getYAxes = function () { return yaxes; }; - plot.c2p = canvasToAxisCoords; - plot.p2c = axisToCanvasCoords; - plot.getOptions = function () { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { - left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), - top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) - }; - }; - plot.shutdown = shutdown; - plot.destroy = function () { - shutdown(); - placeholder.removeData("plot").empty(); - - series = []; - options = null; - surface = null; - overlay = null; - eventHolder = null; - ctx = null; - octx = null; - xaxes = []; - yaxes = []; - hooks = null; - highlights = []; - plot = null; - }; - plot.resize = function () { - var width = placeholder.width(), - height = placeholder.height(); - surface.resize(width, height); - overlay.resize(width, height); - }; - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - setupCanvases(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - - // References to key classes, allowing plugins to modify them - - var classes = { - Canvas: Canvas - }; - - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot, classes); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - - $.extend(true, options, opts); - - // $.extend merges arrays, rather than replacing them. When less - // colors are provided than the size of the default palette, we - // end up with those colors plus the remaining defaults, which is - // not expected behavior; avoid it by replacing them here. - - if (opts && opts.colors) { - options.colors = opts.colors; - } - - if (options.xaxis.color == null) - options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - if (options.yaxis.color == null) - options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility - options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; - if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility - options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; - - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - // Fill in defaults for axis options, including any unspecified - // font-spec fields, if a font-spec was provided. - - // If no x/y axis options were provided, create one of each anyway, - // since the rest of the code assumes that they exist. - - var i, axisOptions, axisCount, - fontSize = placeholder.css("font-size"), - fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, - fontDefaults = { - style: placeholder.css("font-style"), - size: Math.round(0.8 * fontSizeDefault), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; - - axisCount = options.xaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.xaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.xaxis, axisOptions); - options.xaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - axisCount = options.yaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.yaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.yaxis, axisOptions); - options.yaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.x2axis) { - options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); - options.xaxes[1].position = "top"; - // Override the inherit to allow the axis to auto-scale - if (options.x2axis.min == null) { - options.xaxes[1].min = null; - } - if (options.x2axis.max == null) { - options.xaxes[1].max = null; - } - } - if (options.y2axis) { - options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); - options.yaxes[1].position = "right"; - // Override the inherit to allow the axis to auto-scale - if (options.y2axis.min == null) { - options.yaxes[1].min = null; - } - if (options.y2axis.max == null) { - options.yaxes[1].max = null; - } - } - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) - options.series.shadowSize = options.shadowSize; - if (options.highlightColor != null) - options.series.highlightColor = options.highlightColor; - - // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) - getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) - getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; - - // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data != null) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisNumber(obj, coord) { - var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number - a = a.n; - if (typeof a != "number") - a = 1; // default to first axis - return a; - } - - function allAxes() { - // return flat array without annoying null entries - return $.grep(xaxes.concat(yaxes), function (a) { return a; }); - } - - function canvasToAxisCoords(pos) { - // return an object with x/y corresponding to all used axes - var res = {}, i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res["x" + axis.n] = axis.c2p(pos.left); - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res["y" + axis.n] = axis.c2p(pos.top); - } - - if (res.x1 !== undefined) - res.x = res.x1; - if (res.y1 !== undefined) - res.y = res.y1; - - return res; - } - - function axisToCanvasCoords(pos) { - // get canvas coords from the first pair of x/y found in pos - var res = {}, i, axis, key; - - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) { - key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "x"; - - if (pos[key] != null) { - res.left = axis.p2c(pos[key]); - break; - } - } - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) { - key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "y"; - - if (pos[key] != null) { - res.top = axis.p2c(pos[key]); - break; - } - } - } - - return res; - } - - function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) - axes[number - 1] = { - n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) - }; - - return axes[number - 1]; - } - - function fillInSeriesOptions() { - - var neededColors = series.length, maxIndex = -1, i; - - // Subtract the number of series that already have fixed colors or - // color indexes from the number that we still need to generate. - - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - neededColors--; - if (typeof sc == "number" && sc > maxIndex) { - maxIndex = sc; - } - } - } - - // If any of the series have fixed color indexes, then we need to - // generate at least as many colors as the highest index. - - if (neededColors <= maxIndex) { - neededColors = maxIndex + 1; - } - - // Generate all the colors, using first the option colors and then - // variations on those colors once they're exhausted. - - var c, colors = [], colorPool = options.colors, - colorPoolSize = colorPool.length, variation = 0; - - for (i = 0; i < neededColors; i++) { - - c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); - - // Each time we exhaust the colors in the pool we adjust - // a scaling factor used to produce more variations on - // those colors. The factor alternates negative/positive - // to produce lighter/darker colors. - - // Reset the variation after every few cycles, or else - // it will end up producing only white or black colors. - - if (i % colorPoolSize == 0 && i) { - if (variation >= 0) { - if (variation < 0.5) { - variation = -variation - 0.2; - } else variation = 0; - } else variation = -variation; - } - - colors[i] = c.scale('rgb', 1 + variation); - } - - // Finalize the series options, filling in their colors - - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v] && s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // If nothing was provided for lines.zero, default it to match - // lines.fill, since areas by default should extend to zero. - - if (s.lines.zero == null) { - s.lines.zero = !!s.lines.fill; - } - - // setup axes - s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); - s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p, - data, format; - - function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) - axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) - axis.datamax = max; - } - - $.each(allAxes(), function (_, axis) { - // init axis - axis.datamin = topSentry; - axis.datamax = bottomSentry; - axis.used = false; - }); - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - data = s.data; - format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - var insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - else if (val == Infinity) - val = fakeInfinity; - else if (val == -Infinity) - val = -fakeInfinity; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.autoscale !== false) { - if (f.x) { - updateAxis(s.xaxis, val, val); - } - if (f.y) { - updateAxis(s.yaxis, val, val); - } - } - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points; - ps = s.datapoints.pointsize; - format = s.datapoints.format; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta; - - switch (s.bars.align) { - case "left": - delta = 0; - break; - case "right": - delta = -s.bars.barWidth; - break; - default: - delta = -s.bars.barWidth / 2; - } - - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) - axis.datamin = null; - if (axis.datamax == bottomSentry) - axis.datamax = null; - }); - } - - function setupCanvases() { - - // Make sure the placeholder is clear of everything except canvases - // from a previous plot in this container that we'll try to re-use. - - placeholder.css("padding", 0) // padding messes up the positioning - .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); - }).remove(); - - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - surface = new Canvas("flot-base", placeholder); - overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features - - ctx = surface.context; - octx = overlay.context; - - // define which element we're listening for events on - eventHolder = $(overlay.element).unbind(); - - // If we're re-using a plot object, shut down the old one - - var existing = placeholder.data("plot"); - - if (existing) { - existing.shutdown(); - overlay.clear(); - } - - // save in case we get replotted - placeholder.data("plot", plot); - } - - function bindEvents() { - // bind events - if (options.grid.hoverable) { - eventHolder.mousemove(onMouseMove); - - // Use bind, rather than .mouseleave, because we officially - // still support jQuery 1.2.6, which doesn't define a shortcut - // for mouseenter or mouseleave. This was a bug/oversight that - // was fixed somewhere around 1.3.x. We can return to using - // .mouseleave when we drop support for 1.2.6. - - eventHolder.bind("mouseleave", onMouseLeave); - } - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function shutdown() { - if (redrawTimeout) - clearTimeout(redrawTimeout); - - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mouseleave", onMouseLeave); - eventHolder.unbind("click", onClick); - - executeHooks(hooks.shutdown, [eventHolder]); - } - - function setTransformationHelpers(axis) { - // set helper functions on the axis, assumes plot area - // has been computed already - - function identity(x) { return x; } - - var s, m, t = axis.options.transform || identity, - it = axis.options.inverseTransform; - - // precompute how much the axis is scaling a point - // in canvas space - if (axis.direction == "x") { - s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); - m = Math.min(t(axis.max), t(axis.min)); - } - else { - s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); - s = -s; - m = Math.max(t(axis.max), t(axis.min)); - } - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - - function measureTickLabels(axis) { - - var opts = axis.options, - ticks = axis.ticks || [], - labelWidth = opts.labelWidth || 0, - labelHeight = opts.labelHeight || 0, - maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = opts.font || "flot-tick-label tickLabel"; - - for (var i = 0; i < ticks.length; ++i) { - - var t = ticks[i]; - - if (!t.label) - continue; - - var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - - labelWidth = Math.max(labelWidth, info.width); - labelHeight = Math.max(labelHeight, info.height); - } - - axis.labelWidth = opts.labelWidth || labelWidth; - axis.labelHeight = opts.labelHeight || labelHeight; - } - - function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset; this first phase only looks at one - // dimension per axis, the other dimension depends on the - // other axes so will have to wait - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - isXAxis = axis.direction === "x", - tickLength = axis.options.tickLength, - axisMargin = options.grid.axisMargin, - padding = options.grid.labelMargin, - innermost = true, - outermost = true, - first = true, - found = false; - - // Determine the axis's position in its direction and on its side - - $.each(isXAxis ? xaxes : yaxes, function(i, a) { - if (a && (a.show || a.reserveSpace)) { - if (a === axis) { - found = true; - } else if (a.options.position === pos) { - if (found) { - outermost = false; - } else { - innermost = false; - } - } - if (!found) { - first = false; - } - } - }); - - // The outermost axis on each side has no margin - - if (outermost) { - axisMargin = 0; - } - - // The ticks for the first axis in each direction stretch across - - if (tickLength == null) { - tickLength = first ? "full" : 5; - } - - if (!isNaN(+tickLength)) - padding += +tickLength; - - if (isXAxis) { - lh += padding; - - if (pos == "bottom") { - plotOffset.bottom += lh + axisMargin; - axis.box = { top: surface.height - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axisMargin, height: lh }; - plotOffset.top += lh + axisMargin; - } - } - else { - lw += padding; - - if (pos == "left") { - axis.box = { left: plotOffset.left + axisMargin, width: lw }; - plotOffset.left += lw + axisMargin; - } - else { - plotOffset.right += lw + axisMargin; - axis.box = { left: surface.width - plotOffset.right, width: lw }; - } - } - - // save for future reference - axis.position = pos; - axis.tickLength = tickLength; - axis.box.padding = padding; - axis.innermost = innermost; - } - - function allocateAxisBoxSecondPhase(axis) { - // now that all axis boxes have been placed in one - // dimension, we can set the remaining dimension coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; - } - else { - axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; - } - } - - function adjustLayoutForThingsStickingOut() { - // possibly adjust plot offset to ensure everything stays - // inside the canvas and isn't clipped off - - var minMargin = options.grid.minBorderMargin, - axis, i; - - // check stuff from the plot (FIXME: this should just read - // a value from the series, otherwise it's impossible to - // customize) - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); - } - - var margins = { - left: minMargin, - right: minMargin, - top: minMargin, - bottom: minMargin - }; - - // check axis labels, note we don't check the actual - // labels but instead use the overall width/height to not - // jump as much around with replots - $.each(allAxes(), function (_, axis) { - if (axis.reserveSpace && axis.ticks && axis.ticks.length) { - if (axis.direction === "x") { - margins.left = Math.max(margins.left, axis.labelWidth / 2); - margins.right = Math.max(margins.right, axis.labelWidth / 2); - } else { - margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); - margins.top = Math.max(margins.top, axis.labelHeight / 2); - } - } - }); - - plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); - plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); - plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); - plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); - } - - function setupGrid() { - var i, axes = allAxes(), showGrid = options.grid.show; - - // Initialize the plot's offset from the edge of the canvas - - for (var a in plotOffset) { - var margin = options.grid.margin || 0; - plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; - } - - executeHooks(hooks.processOffset, [plotOffset]); - - // If the grid is visible, add its border width to the offset - - for (var a in plotOffset) { - if(typeof(options.grid.borderWidth) == "object") { - plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; - } - else { - plotOffset[a] += showGrid ? options.grid.borderWidth : 0; - } - } - - $.each(axes, function (_, axis) { - var axisOpts = axis.options; - axis.show = axisOpts.show == null ? axis.used : axisOpts.show; - axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; - setRange(axis); - }); - - if (showGrid) { - - var allocatedAxes = $.grep(axes, function (axis) { - return axis.show || axis.reserveSpace; - }); - - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - measureTickLabels(axis); - }); - - // with all dimensions calculated, we can compute the - // axis bounding boxes, start from the outside - // (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) - allocateAxisBoxFirstPhase(allocatedAxes[i]); - - // make sure we've got enough space for things that - // might stick out - adjustLayoutForThingsStickingOut(); - - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - } - - plotWidth = surface.width - plotOffset.left - plotOffset.right; - plotHeight = surface.height - plotOffset.bottom - plotOffset.top; - - // now we got the proper plot dimensions, we can compute the scaling - $.each(axes, function (_, axis) { - setTransformationHelpers(axis); - }); - - if (showGrid) { - drawAxisLabels(); - } - - insertLegend(); - } - - function setRange(axis) { - var opts = axis.options, - min = +(opts.min != null ? opts.min : axis.datamin), - max = +(opts.max != null ? opts.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (opts.min == null) - min -= widen; - // always widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = opts.autoscaleMargin; - if (margin != null) { - if (opts.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (opts.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function setupTickGeneration(axis) { - var opts = axis.options; - - // estimate number of ticks - var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) - noTicks = opts.ticks; - else - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); - - var delta = (axis.max - axis.min) / noTicks, - dec = -Math.floor(Math.log(delta) / Math.LN10), - maxDec = opts.tickDecimals; - - if (maxDec != null && dec > maxDec) { - dec = maxDec; - } - - var magn = Math.pow(10, -dec), - norm = delta / magn, // norm is between 1.0 and 10.0 - size; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) { - size = opts.minTickSize; - } - - axis.delta = delta; - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - - // Time mode was moved to a plug-in in 0.8, and since so many people use it - // we'll add an especially friendly reminder to make sure they included it. - - if (opts.mode == "time" && !axis.tickGenerator) { - throw new Error("Time mode requires the flot.time plugin."); - } - - // Flot supports base-10 axes; any other mode else is handled by a plug-in, - // like flot.time.js. - - if (!axis.tickGenerator) { - - axis.tickGenerator = function (axis) { - - var ticks = [], - start = floorInBase(axis.min, axis.tickSize), - i = 0, - v = Number.NaN, - prev; - - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push(v); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - var formatted = "" + Math.round(value * factor) / factor; - - // If tickDecimals was specified, ensure that we have exactly that - // much precision; otherwise default to the value's own precision. - - if (axis.tickDecimals != null) { - var decimal = formatted.indexOf("."); - var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - - return formatted; - }; - } - - if ($.isFunction(opts.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; - - if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { - // consider snapping min/max to outermost nice ticks - var niceTicks = axis.tickGenerator(axis); - if (niceTicks.length > 0) { - if (opts.min == null) - axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) - axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); - } - - axis.tickGenerator = function (axis) { - // copy ticks, scaled to this axis - var ticks = [], v, i; - for (i = 0; i < otherAxis.ticks.length; ++i) { - v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); - v = axis.min + v * (axis.max - axis.min); - ticks.push(v); - } - return ticks; - }; - - // we might need an extra decimal since forced - // ticks don't necessarily fit naturally - if (!axis.mode && opts.tickDecimals == null) { - var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), - ts = axis.tickGenerator(axis); - - // only proceed if the tick interval rounded - // with an extra decimal doesn't give us a - // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) - axis.tickDecimals = extraDec; - } - } - } - } - - function setTicks(axis) { - var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) - ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) - // generate the ticks - ticks = oticks(axis); - else - ticks = oticks; - } - - // clean up/labelify the supplied ticks, copy them over - var i, v; - axis.ticks = []; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = +t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = +t; - if (label == null) - label = axis.tickFormatter(v, axis); - if (!isNaN(v)) - axis.ticks.push({ v: v, label: label }); - } - } - - function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin && ticks.length > 0) { - // snap to ticks - if (axis.options.min == null) - axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) - axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); - } - } - - function draw() { - - surface.clear(); - - executeHooks(hooks.drawBackground, [ctx]); - - var grid = options.grid; - - // draw background, if any - if (grid.show && grid.backgroundColor) - drawBackground(); - - if (grid.show && !grid.aboveData) { - drawGrid(); - } - - for (var i = 0; i < series.length; ++i) { - executeHooks(hooks.drawSeries, [ctx, series[i]]); - drawSeries(series[i]); - } - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) { - drawGrid(); - } - - surface.render(); - - // A draw implies that either the axes or data have changed, so we - // should probably update the overlay highlights as well. - - triggerRedrawOverlay(); - } - - function extractRange(ranges, coord) { - var axis, from, to, key, axes = allAxes(); - - for (var i = 0; i < axes.length; ++i) { - axis = axes[i]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function drawBackground() { - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - ctx.restore(); - } - - function drawGrid() { - var i, axes, bw, bc; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) { - axes = plot.getAxes(); - // xmin etc. is backwards compatibility, to be - // removed in the future - axes.xmin = axes.xaxis.min; - axes.xmax = axes.xaxis.max; - axes.ymin = axes.yaxis.min; - axes.ymax = axes.yaxis.max; - - markings = markings(axes); - } - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - var xequal = xrange.from === xrange.to, - yequal = yrange.from === yrange.to; - - if (xequal && yequal) { - continue; - } - - // then draw - xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); - xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); - yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); - yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); - - if (xequal || yequal) { - var lineWidth = m.lineWidth || options.grid.markingsLineWidth, - subPixel = lineWidth % 2 ? 0.5 : 0; - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = lineWidth; - if (xequal) { - ctx.moveTo(xrange.to + subPixel, yrange.from); - ctx.lineTo(xrange.to + subPixel, yrange.to); - } else { - ctx.moveTo(xrange.from, yrange.to + subPixel); - ctx.lineTo(xrange.to, yrange.to + subPixel); - } - ctx.stroke(); - } else { - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the ticks - axes = allAxes(); - bw = options.grid.borderWidth; - - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box, - t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) - continue; - - ctx.lineWidth = 1; - - // find the edges - if (axis.direction == "x") { - x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { - y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); - } - - // draw tick bar - if (!axis.innermost) { - ctx.strokeStyle = axis.options.color; - ctx.beginPath(); - xoff = yoff = 0; - if (axis.direction == "x") - xoff = plotWidth + 1; - else - yoff = plotHeight + 1; - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") { - y = Math.floor(y) + 0.5; - } else { - x = Math.floor(x) + 0.5; - } - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - ctx.stroke(); - } - - // draw ticks - - ctx.strokeStyle = axis.options.tickColor; - - ctx.beginPath(); - for (i = 0; i < axis.ticks.length; ++i) { - var v = axis.ticks[i].v; - - xoff = yoff = 0; - - if (isNaN(v) || v < axis.min || v > axis.max - // skip those lying on the axes if we got a border - || (t == "full" - && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) - && (v == axis.min || v == axis.max))) - continue; - - if (axis.direction == "x") { - x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; - - if (axis.position == "top") - yoff = -yoff; - } - else { - y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; - - if (axis.position == "left") - xoff = -xoff; - } - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") - x = Math.floor(x) + 0.5; - else - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - } - - ctx.stroke(); - } - - - // draw border - if (bw) { - // If either borderWidth or borderColor is an object, then draw the border - // line by line instead of as one rectangle - bc = options.grid.borderColor; - if(typeof bw == "object" || typeof bc == "object") { - if (typeof bw !== "object") { - bw = {top: bw, right: bw, bottom: bw, left: bw}; - } - if (typeof bc !== "object") { - bc = {top: bc, right: bc, bottom: bc, left: bc}; - } - - if (bw.top > 0) { - ctx.strokeStyle = bc.top; - ctx.lineWidth = bw.top; - ctx.beginPath(); - ctx.moveTo(0 - bw.left, 0 - bw.top/2); - ctx.lineTo(plotWidth, 0 - bw.top/2); - ctx.stroke(); - } - - if (bw.right > 0) { - ctx.strokeStyle = bc.right; - ctx.lineWidth = bw.right; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); - ctx.lineTo(plotWidth + bw.right / 2, plotHeight); - ctx.stroke(); - } - - if (bw.bottom > 0) { - ctx.strokeStyle = bc.bottom; - ctx.lineWidth = bw.bottom; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); - ctx.lineTo(0, plotHeight + bw.bottom / 2); - ctx.stroke(); - } - - if (bw.left > 0) { - ctx.strokeStyle = bc.left; - ctx.lineWidth = bw.left; - ctx.beginPath(); - ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); - ctx.lineTo(0- bw.left/2, 0); - ctx.stroke(); - } - } - else { - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - } - - ctx.restore(); - } - - function drawAxisLabels() { - - $.each(allAxes(), function (_, axis) { - var box = axis.box, - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = axis.options.font || "flot-tick-label tickLabel", - tick, x, y, halign, valign; - - // Remove text before checking for axis.show and ticks.length; - // otherwise plugins, like flot-tickrotor, that draw their own - // tick labels will end up with both theirs and the defaults. - - surface.removeText(layer); - - if (!axis.show || axis.ticks.length == 0) - return; - - for (var i = 0; i < axis.ticks.length; ++i) { - - tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - - if (axis.direction == "x") { - halign = "center"; - x = plotOffset.left + axis.p2c(tick.v); - if (axis.position == "bottom") { - y = box.top + box.padding; - } else { - y = box.top + box.height - box.padding; - valign = "bottom"; - } - } else { - valign = "middle"; - y = plotOffset.top + axis.p2c(tick.v); - if (axis.position == "left") { - x = box.left + box.width - box.padding; - halign = "right"; - } else { - x = box.left + box.padding; - } - } - - surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); - } - }); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, - ypos = 1, segmentStart = 0, segmentEnd = 0; - - // we process each segment in two turns, first forward - // direction to sketch out top, then once we hit the - // end we go backwards to sketch the bottom - while (true) { - if (ps > 0 && i > points.length + ps) - break; - - i += ps; // ps is negative if going backwards - - var x1 = points[i - ps], - y1 = points[i - ps + ypos], - x2 = points[i], y2 = points[i + ypos]; - - if (areaOpen) { - if (ps > 0 && x1 != null && x2 == null) { - // at turning point - segmentEnd = i; - ps = -ps; - ypos = 2; - continue; - } - - if (ps < 0 && i == segmentStart + ps) { - // done with the reverse sweep - ctx.fill(); - areaOpen = false; - ps = -ps; - ypos = 1; - i = segmentStart = segmentEnd + ps; - continue; - } - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - continue; - } - - // else it's a bit more complicated, there might - // be a flat maxed out rectangle first, then a - // triangular cutout or reverse; to find these - // keep track of the current x values - var x1old = x1, x2old = x2; - - // clip the y values, without shortcutting, we - // go through all cases in turn - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); - // it goes to (x1, y1), but we fill that below - } - - // fill triangular section, this sometimes result - // in redundant points if (x1, y1) hasn't changed - // from previous line to, but we just ignore that - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); - } - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - x = axisx.p2c(x); - y = axisy.p2c(y) + offset; - if (symbol == "circle") - ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else - symbol(ctx, x, y, radius, shadow); - ctx.closePath(); - - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.points.lineWidth, - sw = series.shadowSize, - radius = series.points.radius, - symbol = series.points.symbol; - - // If the user sets the line width to 0, we change it to a very - // small value. A line width of 0 seems to force the default of 1. - // Doing the conditional here allows the shadow setting to still be - // optional even with a lineWidth of 0. - - if( lw == 0 ) - lw = 0.0001; - - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, true, - series.xaxis, series.yaxis, symbol); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, true, - series.xaxis, series.yaxis, symbol); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, false, - series.xaxis, series.yaxis, symbol); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - // in horizontal mode, we start the bar from the left - // instead of from the bottom so it appears to be - // horizontal rather than vertical - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.fillStyle = fillStyleCallback(bottom, top); - c.fillRect(left, top, right - left, bottom - top) - } - - // draw outline - if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom); - if (drawLeft) - c.lineTo(left, top); - else - c.moveTo(left, top); - if (drawTop) - c.lineTo(right, top); - else - c.moveTo(right, top); - if (drawRight) - c.lineTo(right, bottom); - else - c.moveTo(right, bottom); - if (drawBottom) - c.lineTo(left, bottom); - else - c.moveTo(left, bottom); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - - var barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - - if (options.legend.container != null) { - $(options.legend.container).html(""); - } else { - placeholder.find(".legend").remove(); - } - - if (!options.legend.show) { - return; - } - - var fragments = [], entries = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - - // Build a list of legend entries, with each having a label and a color - - for (var i = 0; i < series.length; ++i) { - s = series[i]; - if (s.label) { - label = lf ? lf(s.label, s) : s.label; - if (label) { - entries.push({ - label: label, - color: s.color - }); - } - } - } - - // Sort the legend using either the default or a custom comparator - - if (options.legend.sorted) { - if ($.isFunction(options.legend.sorted)) { - entries.sort(options.legend.sorted); - } else if (options.legend.sorted == "reverse") { - entries.reverse(); - } else { - var ascending = options.legend.sorted != "descending"; - entries.sort(function(a, b) { - return a.label == b.label ? 0 : ( - (a.label < b.label) != ascending ? 1 : -1 // Logical XOR - ); - }); - } - } - - // Generate markup for the list of entries, in their final order - - for (var i = 0; i < entries.length; ++i) { - - var entry = entries[i]; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; - } - - fragments.push( - '
' + - '' + entry.label + '' - ); - } - - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j, ps; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - ps = s.datapoints.pointsize; - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - - var barLeft, barRight; - - switch (s.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -s.bars.barWidth; - break; - default: - barLeft = -s.bars.barWidth / 2; - } - - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - var t = options.interaction.redrawOverlayInterval; - if (t == -1) { // skip event queue - drawOverlay(); - return; - } - - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, t); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - overlay.clear(); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - return; - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis, - highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = highlightColor; - var radius = 1.5 * pointRadius; - x = axisx.p2c(x); - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), - fillStyle = highlightColor, - barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = highlightColor; - - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness); - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - // Add the plot function to the top level of the jQuery object - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.8.3"; - - $.plot.plugins = []; - - // Also add the plot function as a chainable property - - $.fn.plot = function(data, options) { - return this.each(function() { - $.plot(this, data, options); - }); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.log.js b/src/legacy/ui/public/flot-charts/jquery.flot.log.js deleted file mode 100644 index e32bf5cf7e817..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.log.js +++ /dev/null @@ -1,163 +0,0 @@ -/* @notice - * - * Pretty handling of logarithmic axes. - * Copyright (c) 2007-2014 IOLA and Ole Laursen. - * Licensed under the MIT license. - * Created by Arne de Laat - * Set axis.mode to "log" and make the axis logarithmic using transform: - * axis: { - * mode: 'log', - * transform: function(v) {v <= 0 ? Math.log(v) / Math.LN10 : null}, - * inverseTransform: function(v) {Math.pow(10, v)} - * } - * The transform filters negative and zero values, because those are - * invalid on logarithmic scales. - * This plugin tries to create good looking logarithmic ticks, using - * unicode superscript characters. If all data to be plotted is between two - * powers of ten then the default flot tick generator and renderer are - * used. Logarithmic ticks are places at powers of ten and at half those - * values if there are not to many ticks already (e.g. [1, 5, 10, 50, 100]). - * For details, see https://github.com/flot/flot/pull/1328 -*/ - -(function($) { - - function log10(value) { - /* Get the Log10 of the value - */ - return Math.log(value) / Math.LN10; - } - - function floorAsLog10(value) { - /* Get power of the first power of 10 below the value - */ - return Math.floor(log10(value)); - } - - function ceilAsLog10(value) { - /* Get power of the first power of 10 above the value - */ - return Math.ceil(log10(value)); - } - - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - function getUnicodePower(power) { - var superscripts = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"], - result = "", - str_power = "" + power; - for (var i = 0; i < str_power.length; i++) { - if (str_power[i] === "+") { - } - else if (str_power[i] === "-") { - result += "⁻"; - } - else { - result += superscripts[str_power[i]]; - } - } - return result; - } - - function init(plot) { - plot.hooks.processOptions.push(function (plot) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode === "log") { - - axis.tickGenerator = function (axis) { - - var ticks = [], - end = ceilAsLog10(axis.max), - start = floorAsLog10(axis.min), - tick = Number.NaN, - i = 0; - - if (axis.min === null || axis.min <= 0) { - // Bad minimum, make ticks from 1 (10**0) to max - start = 0; - axis.min = 0.6; - } - - if (end <= start) { - // Start less than end?! - ticks = [1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, - 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, - 1e7, 1e8, 1e9]; - } - else if (log10(axis.max) - log10(axis.datamin) < 1) { - // Default flot generator incase no powers of 10 - // are between start and end - var prev; - start = floorInBase(axis.min, axis.tickSize); - do { - prev = tick; - tick = start + i * axis.tickSize; - ticks.push(tick); - ++i; - } while (tick < axis.max && tick !== prev); - } - else { - // Make ticks at each power of ten - for (; i <= (end - start); i++) { - tick = Math.pow(10, start + i); - ticks.push(tick); - } - - var length = ticks.length; - - // If not to many ticks also put a tick between - // the powers of ten - if (end - start < 6) { - for (var j = 1; j < length * 2 - 1; j += 2) { - tick = ticks[j - 1] * 5; - ticks.splice(j, 0, tick); - } - } - } - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - var formatted; - if (log10(axis.max) - log10(axis.datamin) < 1) { - // Default flot formatter - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - formatted = "" + Math.round(value * factor) / factor; - if (axis.tickDecimals !== null) { - var decimal = formatted.indexOf("."); - var precision = decimal === -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - } - else { - var multiplier = "", - exponential = parseFloat(value).toExponential(0), - power = getUnicodePower(exponential.slice(2)); - if (exponential[0] !== "1") { - multiplier = exponential[0] + "x"; - } - formatted = multiplier + "10" + power; - } - return formatted; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - name: "log", - version: "0.9" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.navigate.js b/src/legacy/ui/public/flot-charts/jquery.flot.navigate.js deleted file mode 100644 index 13fb7f17d04b2..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.navigate.js +++ /dev/null @@ -1,346 +0,0 @@ -/* Flot plugin for adding the ability to pan and zoom the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The default behaviour is double click and scrollwheel up/down to zoom in, drag -to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and -plot.pan( offset ) so you easily can add custom controls. It also fires -"plotpan" and "plotzoom" events, useful for synchronizing plots. - -The plugin supports these options: - - zoom: { - interactive: false - trigger: "dblclick" // or "click" for single click - amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out) - } - - pan: { - interactive: false - cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer" - frameRate: 20 - } - - xaxis, yaxis, x2axis, y2axis: { - zoomRange: null // or [ number, number ] (min range, max range) or false - panRange: null // or [ number, number ] (min, max) or false - } - -"interactive" enables the built-in drag/click behaviour. If you enable -interactive for pan, then you'll have a basic plot that supports moving -around; the same for zoom. - -"amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to -the current viewport. - -"cursor" is a standard CSS mouse cursor string used for visual feedback to the -user when dragging. - -"frameRate" specifies the maximum number of times per second the plot will -update itself while the user is panning around on it (set to null to disable -intermediate pans, the plot will then not update until the mouse button is -released). - -"zoomRange" is the interval in which zooming can happen, e.g. with zoomRange: -[1, 100] the zoom will never scale the axis so that the difference between min -and max is smaller than 1 or larger than 100. You can set either end to null -to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis -will be disabled. - -"panRange" confines the panning to stay within a range, e.g. with panRange: -[-10, 20] panning stops at -10 in one end and at 20 in the other. Either can -be null, e.g. [-10, null]. If you set panRange to false, panning on that axis -will be disabled. - -Example API usage: - - plot = $.plot(...); - - // zoom default amount in on the pixel ( 10, 20 ) - plot.zoom({ center: { left: 10, top: 20 } }); - - // zoom out again - plot.zoomOut({ center: { left: 10, top: 20 } }); - - // zoom 200% in on the pixel (10, 20) - plot.zoom({ amount: 2, center: { left: 10, top: 20 } }); - - // pan 100 pixels to the left and 20 down - plot.pan({ left: -100, top: 20 }) - -Here, "center" specifies where the center of the zooming should happen. Note -that this is defined in pixel space, not the space of the data points (you can -use the p2c helpers on the axes in Flot to help you convert between these). - -"amount" is the amount to zoom the viewport relative to the current range, so -1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You -can set the default in the options. - -*/ - -// First two dependencies, jquery.event.drag.js and -// jquery.mousewheel.js, we put them inline here to save people the -// effort of downloading them. - -/* -jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com) -Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt -*/ -(function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY) max) { - // make sure min < max - var tmp = min; - min = max; - max = tmp; - } - - //Check that we are in panRange - if (pr) { - if (pr[0] != null && min < pr[0]) { - min = pr[0]; - } - if (pr[1] != null && max > pr[1]) { - max = pr[1]; - } - } - - var range = max - min; - if (zr && - ((zr[0] != null && range < zr[0] && amount >1) || - (zr[1] != null && range > zr[1] && amount <1))) - return; - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotzoom", [ plot, args ]); - }; - - plot.pan = function (args) { - var delta = { - x: +args.left, - y: +args.top - }; - - if (isNaN(delta.x)) - delta.x = 0; - if (isNaN(delta.y)) - delta.y = 0; - - $.each(plot.getAxes(), function (_, axis) { - var opts = axis.options, - min, max, d = delta[axis.direction]; - - min = axis.c2p(axis.p2c(axis.min) + d), - max = axis.c2p(axis.p2c(axis.max) + d); - - var pr = opts.panRange; - if (pr === false) // no panning on this axis - return; - - if (pr) { - // check whether we hit the wall - if (pr[0] != null && pr[0] > min) { - d = pr[0] - min; - min += d; - max += d; - } - - if (pr[1] != null && pr[1] < max) { - d = pr[1] - max; - min += d; - max += d; - } - } - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotpan", [ plot, args ]); - }; - - function shutdown(plot, eventHolder) { - eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick); - eventHolder.unbind("mousewheel", onMouseWheel); - eventHolder.unbind("dragstart", onDragStart); - eventHolder.unbind("drag", onDrag); - eventHolder.unbind("dragend", onDragEnd); - if (panTimeout) - clearTimeout(panTimeout); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'navigate', - version: '1.3' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.pie.js b/src/legacy/ui/public/flot-charts/jquery.flot.pie.js deleted file mode 100644 index 06f900bdc950f..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.pie.js +++ /dev/null @@ -1,824 +0,0 @@ -/* Flot plugin for rendering pie charts. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes that each series has a single data value, and that each -value is a positive integer or zero. Negative numbers don't make sense for a -pie chart, and have unpredictable results. The values do NOT need to be -passed in as percentages; the plugin will calculate the total and per-slice -percentages internally. - -* Created by Brian Medendorp - -* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars - -The plugin supports these options: - - series: { - pie: { - show: true/false - radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' - innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect - startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result - tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) - offset: { - top: integer value to move the pie up or down - left: integer value to move the pie left or right, or 'auto' - }, - stroke: { - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#FFF') - width: integer pixel width of the stroke - }, - label: { - show: true/false, or 'auto' - formatter: a user-defined function that modifies the text/style of the label text - radius: 0-1 for percentage of fullsize, or a specified pixel length - background: { - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#000') - opacity: 0-1 - }, - threshold: 0-1 for the percentage value at which to hide labels (if they're too small) - }, - combine: { - threshold: 0-1 for the percentage value at which to combine slices (if they're too small) - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined - label: any text value of what the combined slice should be labeled - } - highlight: { - opacity: 0-1 - } - } - } - -More detail and specific examples can be found in the included HTML file. - -*/ - -import { i18n } from '@kbn/i18n'; - -(function($) { - // Maximum redraw attempts when fitting labels within the plot - - var REDRAW_ATTEMPTS = 10; - - // Factor by which to shrink the pie when fitting labels within the plot - - var REDRAW_SHRINK = 0.95; - - function init(plot) { - - var canvas = null, - target = null, - options = null, - maxRadius = null, - centerLeft = null, - centerTop = null, - processed = false, - ctx = null; - - // interactive variables - - var highlights = []; - - // add hook to determine if pie plugin in enabled, and then perform necessary operations - - plot.hooks.processOptions.push(function(plot, options) { - if (options.series.pie.show) { - - options.grid.show = false; - - // set labels.show - - if (options.series.pie.label.show == "auto") { - if (options.legend.show) { - options.series.pie.label.show = false; - } else { - options.series.pie.label.show = true; - } - } - - // set radius - - if (options.series.pie.radius == "auto") { - if (options.series.pie.label.show) { - options.series.pie.radius = 3/4; - } else { - options.series.pie.radius = 1; - } - } - - // ensure sane tilt - - if (options.series.pie.tilt > 1) { - options.series.pie.tilt = 1; - } else if (options.series.pie.tilt < 0) { - options.series.pie.tilt = 0; - } - } - }); - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var options = plot.getOptions(); - if (options.series.pie.show) { - if (options.grid.hoverable) { - eventHolder.unbind("mousemove").mousemove(onMouseMove); - } - if (options.grid.clickable) { - eventHolder.unbind("click").click(onClick); - } - } - }); - - plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { - var options = plot.getOptions(); - if (options.series.pie.show) { - processDatapoints(plot, series, data, datapoints); - } - }); - - plot.hooks.drawOverlay.push(function(plot, octx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - drawOverlay(plot, octx); - } - }); - - plot.hooks.draw.push(function(plot, newCtx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - draw(plot, newCtx); - } - }); - - function processDatapoints(plot, series, datapoints) { - if (!processed) { - processed = true; - canvas = plot.getCanvas(); - target = $(canvas).parent(); - options = plot.getOptions(); - plot.setData(combine(plot.getData())); - } - } - - function combine(data) { - - var total = 0, - combined = 0, - numCombined = 0, - color = options.series.pie.combine.color, - newdata = []; - - // Fix up the raw data from Flot, ensuring the data is numeric - - for (var i = 0; i < data.length; ++i) { - - var value = data[i].data; - - // If the data is an array, we'll assume that it's a standard - // Flot x-y pair, and are concerned only with the second value. - - // Note how we use the original array, rather than creating a - // new one; this is more efficient and preserves any extra data - // that the user may have stored in higher indexes. - - if ($.isArray(value) && value.length == 1) { - value = value[0]; - } - - if ($.isArray(value)) { - // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 - if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { - value[1] = +value[1]; - } else { - value[1] = 0; - } - } else if (!isNaN(parseFloat(value)) && isFinite(value)) { - value = [1, +value]; - } else { - value = [1, 0]; - } - - data[i].data = [value]; - } - - // Sum up all the slices, so we can calculate percentages for each - - for (var i = 0; i < data.length; ++i) { - total += data[i].data[0][1]; - } - - // Count the number of slices with percentages below the combine - // threshold; if it turns out to be just one, we won't combine. - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (value / total <= options.series.pie.combine.threshold) { - combined += value; - numCombined++; - if (!color) { - color = data[i].color; - } - } - } - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { - newdata.push( - $.extend(data[i], { /* extend to allow keeping all other original data values - and using them e.g. in labelFormatter. */ - data: [[1, value]], - color: data[i].color, - label: data[i].label, - angle: value * Math.PI * 2 / total, - percent: value / (total / 100) - }) - ); - } - } - - if (numCombined > 1) { - newdata.push({ - data: [[1, combined]], - color: color, - label: options.series.pie.combine.label, - angle: combined * Math.PI * 2 / total, - percent: combined / (total / 100) - }); - } - - return newdata; - } - - function draw(plot, newCtx) { - - if (!target) { - return; // if no series were passed - } - - var canvasWidth = plot.getPlaceholder().width(), - canvasHeight = plot.getPlaceholder().height(), - legendWidth = target.children().filter(".legend").children().width() || 0; - - ctx = newCtx; - - // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! - - // When combining smaller slices into an 'other' slice, we need to - // add a new series. Since Flot gives plugins no way to modify the - // list of series, the pie plugin uses a hack where the first call - // to processDatapoints results in a call to setData with the new - // list of series, then subsequent processDatapoints do nothing. - - // The plugin-global 'processed' flag is used to control this hack; - // it starts out false, and is set to true after the first call to - // processDatapoints. - - // Unfortunately this turns future setData calls into no-ops; they - // call processDatapoints, the flag is true, and nothing happens. - - // To fix this we'll set the flag back to false here in draw, when - // all series have been processed, so the next sequence of calls to - // processDatapoints once again starts out with a slice-combine. - // This is really a hack; in 0.9 we need to give plugins a proper - // way to modify series before any processing begins. - - processed = false; - - // calculate maximum radius and center point - - maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; - centerTop = canvasHeight / 2 + options.series.pie.offset.top; - centerLeft = canvasWidth / 2; - - if (options.series.pie.offset.left == "auto") { - if (options.legend.position.match("w")) { - centerLeft += legendWidth / 2; - } else { - centerLeft -= legendWidth / 2; - } - if (centerLeft < maxRadius) { - centerLeft = maxRadius; - } else if (centerLeft > canvasWidth - maxRadius) { - centerLeft = canvasWidth - maxRadius; - } - } else { - centerLeft += options.series.pie.offset.left; - } - - var slices = plot.getData(), - attempts = 0; - - // Keep shrinking the pie's radius until drawPie returns true, - // indicating that all the labels fit, or we try too many times. - - do { - if (attempts > 0) { - maxRadius *= REDRAW_SHRINK; - } - attempts += 1; - clear(); - if (options.series.pie.tilt <= 0.8) { - drawShadow(); - } - } while (!drawPie() && attempts < REDRAW_ATTEMPTS) - - if (attempts >= REDRAW_ATTEMPTS) { - clear(); - const errorMessage = i18n.translate('common.ui.flotCharts.pie.unableToDrawLabelsInsideCanvasErrorMessage', { - defaultMessage: 'Could not draw pie with labels contained inside canvas', - }); - target.prepend(`
${errorMessage}
`); - } - - if (plot.setSeries && plot.insertLegend) { - plot.setSeries(slices); - plot.insertLegend(); - } - - // we're actually done at this point, just defining internal functions at this point - - function clear() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - target.children().filter(".pieLabel, .pieLabelBackground").remove(); - } - - function drawShadow() { - - var shadowLeft = options.series.pie.shadow.left; - var shadowTop = options.series.pie.shadow.top; - var edge = 10; - var alpha = options.series.pie.shadow.alpha; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { - return; // shadow would be outside canvas, so don't draw it - } - - ctx.save(); - ctx.translate(shadowLeft,shadowTop); - ctx.globalAlpha = alpha; - ctx.fillStyle = "#000"; - - // center and rotate to starting position - - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - - //radius -= edge; - - for (var i = 1; i <= edge; i++) { - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, Math.PI * 2, false); - ctx.fill(); - radius -= i; - } - - ctx.restore(); - } - - function drawPie() { - - var startAngle = Math.PI * options.series.pie.startAngle; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - // center and rotate to starting position - - ctx.save(); - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera - - // draw slices - - ctx.save(); - var currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - slices[i].startAngle = currentAngle; - drawSlice(slices[i].angle, slices[i].color, true); - } - ctx.restore(); - - // draw slice outlines - - if (options.series.pie.stroke.width > 0) { - ctx.save(); - ctx.lineWidth = options.series.pie.stroke.width; - currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - drawSlice(slices[i].angle, options.series.pie.stroke.color, false); - } - ctx.restore(); - } - - // draw donut hole - - drawDonutHole(ctx); - - ctx.restore(); - - // Draw the labels, returning true if they fit within the plot - - if (options.series.pie.label.show) { - return drawLabels(); - } else return true; - - function drawSlice(angle, color, fill) { - - if (angle <= 0 || isNaN(angle)) { - return; - } - - if (fill) { - ctx.fillStyle = color; - } else { - ctx.strokeStyle = color; - ctx.lineJoin = "round"; - } - - ctx.beginPath(); - if (Math.abs(angle - Math.PI * 2) > 0.000000001) { - ctx.moveTo(0, 0); // Center of the pie - } - - //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera - ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); - ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); - ctx.closePath(); - //ctx.rotate(angle); // This doesn't work properly in Opera - currentAngle += angle; - - if (fill) { - ctx.fill(); - } else { - ctx.stroke(); - } - } - - function drawLabels() { - - var currentAngle = startAngle; - var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; - - for (var i = 0; i < slices.length; ++i) { - if (slices[i].percent >= options.series.pie.label.threshold * 100) { - if (!drawLabel(slices[i], currentAngle, i)) { - return false; - } - } - currentAngle += slices[i].angle; - } - - return true; - - function drawLabel(slice, startAngle, index) { - - if (slice.data[0][1] == 0) { - return true; - } - - // format label text - - var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; - - if (lf) { - text = lf(slice.label, slice); - } else { - text = slice.label; - } - - if (plf) { - text = plf(text, slice); - } - - var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; - var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); - var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; - - var html = "" + text + ""; - target.append(html); - - var label = target.children("#pieLabel" + index); - var labelTop = (y - label.height() / 2); - var labelLeft = (x - label.width() / 2); - - label.css("top", labelTop); - label.css("left", labelLeft); - - // check to make sure that the label is not outside the canvas - - if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { - return false; - } - - if (options.series.pie.label.background.opacity != 0) { - - // put in the transparent background separately to avoid blended labels and label boxes - - var c = options.series.pie.label.background.color; - - if (c == null) { - c = slice.color; - } - - var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; - $("
") - .css("opacity", options.series.pie.label.background.opacity) - .insertBefore(label); - } - - return true; - } // end individual label function - } // end drawLabels function - } // end drawPie function - } // end draw function - - // Placed here because it needs to be accessed from multiple locations - - function drawDonutHole(layer) { - if (options.series.pie.innerRadius > 0) { - - // subtract the center - - layer.save(); - var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; - layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color - layer.beginPath(); - layer.fillStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.fill(); - layer.closePath(); - layer.restore(); - - // add inner stroke - - layer.save(); - layer.beginPath(); - layer.strokeStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.stroke(); - layer.closePath(); - layer.restore(); - - // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. - } - } - - //-- Additional Interactive related functions -- - - function isPointInPoly(poly, pt) { - for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) - ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) - && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) - && (c = !c); - return c; - } - - function findNearbySlice(mouseX, mouseY) { - - var slices = plot.getData(), - options = plot.getOptions(), - radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, - x, y; - - for (var i = 0; i < slices.length; ++i) { - - var s = slices[i]; - - if (s.pie.show) { - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(0, 0); // Center of the pie - //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. - ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); - ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); - ctx.closePath(); - x = mouseX - centerLeft; - y = mouseY - centerTop; - - if (ctx.isPointInPath) { - if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } else { - - // excanvas for IE doesn;t support isPointInPath, this is a workaround. - - var p1X = radius * Math.cos(s.startAngle), - p1Y = radius * Math.sin(s.startAngle), - p2X = radius * Math.cos(s.startAngle + s.angle / 4), - p2Y = radius * Math.sin(s.startAngle + s.angle / 4), - p3X = radius * Math.cos(s.startAngle + s.angle / 2), - p3Y = radius * Math.sin(s.startAngle + s.angle / 2), - p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), - p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), - p5X = radius * Math.cos(s.startAngle + s.angle), - p5Y = radius * Math.sin(s.startAngle + s.angle), - arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], - arrPoint = [x, y]; - - // TODO: perhaps do some mathematical trickery here with the Y-coordinate to compensate for pie tilt? - - if (isPointInPoly(arrPoly, arrPoint)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } - - ctx.restore(); - } - } - - return null; - } - - function onMouseMove(e) { - triggerClickHoverEvent("plothover", e); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e); - } - - // trigger click or hover event (they send the same parameters so we share their code) - - function triggerClickHoverEvent(eventname, e) { - - var offset = plot.offset(); - var canvasX = parseInt(e.pageX - offset.left); - var canvasY = parseInt(e.pageY - offset.top); - var item = findNearbySlice(canvasX, canvasY); - - if (options.grid.autoHighlight) { - - // clear auto-highlights - - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && !(item && h.series == item.series)) { - unhighlight(h.series); - } - } - } - - // highlight the slice - - if (item) { - highlight(item.series, eventname); - } - - // trigger any hover bind events - - var pos = { pageX: e.pageX, pageY: e.pageY }; - target.trigger(eventname, [pos, item]); - } - - function highlight(s, auto) { - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i == -1) { - highlights.push({ series: s, auto: auto }); - plot.triggerRedrawOverlay(); - } else if (!auto) { - highlights[i].auto = false; - } - } - - function unhighlight(s) { - if (s == null) { - highlights = []; - plot.triggerRedrawOverlay(); - } - - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i != -1) { - highlights.splice(i, 1); - plot.triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s) - return i; - } - return -1; - } - - function drawOverlay(plot, octx) { - - var options = plot.getOptions(); - - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - octx.save(); - octx.translate(centerLeft, centerTop); - octx.scale(1, options.series.pie.tilt); - - for (var i = 0; i < highlights.length; ++i) { - drawHighlight(highlights[i].series); - } - - drawDonutHole(octx); - - octx.restore(); - - function drawHighlight(series) { - - if (series.angle <= 0 || isNaN(series.angle)) { - return; - } - - //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); - octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor - octx.beginPath(); - if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { - octx.moveTo(0, 0); // Center of the pie - } - octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); - octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); - octx.closePath(); - octx.fill(); - } - } - } // end init (plugin body) - - // define pie specific options and their default values - - var options = { - series: { - pie: { - show: false, - radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) - innerRadius: 0, /* for donut */ - startAngle: 3/2, - tilt: 1, - shadow: { - left: 5, // shadow left offset - top: 15, // shadow top offset - alpha: 0.02 // shadow alpha - }, - offset: { - top: 0, - left: "auto" - }, - stroke: { - color: "#fff", - width: 1 - }, - label: { - show: "auto", - formatter: function(label, slice) { - return "
" + label + "
" + Math.round(slice.percent) + "%
"; - }, // formatter function - radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) - background: { - color: null, - opacity: 0 - }, - threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) - }, - combine: { - threshold: -1, // percentage at which to combine little slices into one larger slice - color: null, // color to give the new slice (auto-generated if null) - label: "Other" // label to give the new slice - }, - highlight: { - //color: "#fff", // will add this functionality once parseColor is available - opacity: 0.5 - } - } - } - }; - - $.plot.plugins.push({ - init: init, - options: options, - name: "pie", - version: "1.1" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.resize.js b/src/legacy/ui/public/flot-charts/jquery.flot.resize.js deleted file mode 100644 index 8a626dda0addb..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.resize.js +++ /dev/null @@ -1,59 +0,0 @@ -/* Flot plugin for automatically redrawing plots as the placeholder resizes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -It works by listening for changes on the placeholder div (through the jQuery -resize event plugin) - if the size changes, it will redraw the plot. - -There are no options. If you need to disable the plugin for some plots, you -can just fix the size of their placeholders. - -*/ - -/* Inline dependency: - * jQuery resize event - v1.1 - 3/14/2010 - * http://benalman.com/projects/jquery-resize-plugin/ - * - * Copyright (c) 2010 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - */ -(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); - -(function ($) { - var options = { }; // no options - - function init(plot) { - function onResize() { - var placeholder = plot.getPlaceholder(); - - // somebody might have hidden us and we can't plot - // when we don't have the dimensions - if (placeholder.width() == 0 || placeholder.height() == 0) - return; - - plot.resize(); - plot.setupGrid(); - plot.draw(); - } - - function bindEvents(plot, eventHolder) { - plot.getPlaceholder().resize(onResize); - } - - function shutdown(plot, eventHolder) { - plot.getPlaceholder().unbind("resize", onResize); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'resize', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.selection.js b/src/legacy/ui/public/flot-charts/jquery.flot.selection.js deleted file mode 100644 index c8707b30f4e6f..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.selection.js +++ /dev/null @@ -1,360 +0,0 @@ -/* Flot plugin for selecting regions of a plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - -selection: { - mode: null or "x" or "y" or "xy", - color: color, - shape: "round" or "miter" or "bevel", - minSize: number of pixels -} - -Selection support is enabled by setting the mode to one of "x", "y" or "xy". -In "x" mode, the user will only be able to specify the x range, similarly for -"y" mode. For "xy", the selection becomes a rectangle where both ranges can be -specified. "color" is color of the selection (if you need to change the color -later on, you can get to it with plot.getOptions().selection.color). "shape" -is the shape of the corners of the selection. - -"minSize" is the minimum size a selection can be in pixels. This value can -be customized to determine the smallest size a selection can be and still -have the selection rectangle be displayed. When customizing this value, the -fact that it refers to pixels, not axis units must be taken into account. -Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 -minute, setting "minSize" to 1 will not make the minimum selection size 1 -minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent -"plotunselected" events from being fired when the user clicks the mouse without -dragging. - -When selection support is enabled, a "plotselected" event will be emitted on -the DOM element you passed into the plot function. The event handler gets a -parameter with the ranges selected on the axes, like this: - - placeholder.bind( "plotselected", function( event, ranges ) { - alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) - // similar for yaxis - with multiple axes, the extra ones are in - // x2axis, x3axis, ... - }); - -The "plotselected" event is only fired when the user has finished making the -selection. A "plotselecting" event is fired during the process with the same -parameters as the "plotselected" event, in case you want to know what's -happening while it's happening, - -A "plotunselected" event with no arguments is emitted when the user clicks the -mouse to remove the selection. As stated above, setting "minSize" to 0 will -destroy this behavior. - -The plugin also adds the following methods to the plot object: - -- setSelection( ranges, preventEvent ) - - Set the selection rectangle. The passed in ranges is on the same form as - returned in the "plotselected" event. If the selection mode is "x", you - should put in either an xaxis range, if the mode is "y" you need to put in - an yaxis range and both xaxis and yaxis if the selection mode is "xy", like - this: - - setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); - - setSelection will trigger the "plotselected" event when called. If you don't - want that to happen, e.g. if you're inside a "plotselected" handler, pass - true as the second parameter. If you are using multiple axes, you can - specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of - xaxis, the plugin picks the first one it sees. - -- clearSelection( preventEvent ) - - Clear the selection rectangle. Pass in true to avoid getting a - "plotunselected" event. - -- getSelection() - - Returns the current selection in the same format as the "plotselected" - event. If there's currently no selection, the function returns null. - -*/ - -(function ($) { - function init(plot) { - var selection = { - first: { x: -1, y: -1}, second: { x: -1, y: -1}, - show: false, - active: false - }; - - // FIXME: The drag handling implemented here should be - // abstracted out, there's some similar code from a library in - // the navigation plugin, this should be massaged a bit to fit - // the Flot cases here better and reused. Doing this would - // make this plugin much slimmer. - var savedhandlers = {}; - - var mouseUpHandler = null; - - function onMouseMove(e) { - if (selection.active) { - updateSelection(e); - - plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); - } - } - - function onMouseDown(e) { - if (e.which != 1) // only accept left-click - return; - - // cancel out any text selections - document.body.focus(); - - // prevent text selection and drag in old-school browsers - if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { - savedhandlers.onselectstart = document.onselectstart; - document.onselectstart = function () { return false; }; - } - if (document.ondrag !== undefined && savedhandlers.ondrag == null) { - savedhandlers.ondrag = document.ondrag; - document.ondrag = function () { return false; }; - } - - setSelectionPos(selection.first, e); - - selection.active = true; - - // this is a bit silly, but we have to use a closure to be - // able to whack the same handler again - mouseUpHandler = function (e) { onMouseUp(e); }; - - $(document).one("mouseup", mouseUpHandler); - } - - function onMouseUp(e) { - mouseUpHandler = null; - - // revert drag stuff for old-school browsers - if (document.onselectstart !== undefined) - document.onselectstart = savedhandlers.onselectstart; - if (document.ondrag !== undefined) - document.ondrag = savedhandlers.ondrag; - - // no more dragging - selection.active = false; - updateSelection(e); - - if (selectionIsSane()) - triggerSelectedEvent(); - else { - // this counts as a clear - plot.getPlaceholder().trigger("plotunselected", [ ]); - plot.getPlaceholder().trigger("plotselecting", [ null ]); - } - - return false; - } - - function getSelection() { - if (!selectionIsSane()) - return null; - - if (!selection.show) return null; - - var r = {}, c1 = selection.first, c2 = selection.second; - $.each(plot.getAxes(), function (name, axis) { - if (axis.used) { - var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); - r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; - } - }); - return r; - } - - function triggerSelectedEvent() { - var r = getSelection(); - - plot.getPlaceholder().trigger("plotselected", [ r ]); - - // backwards-compat stuff, to be removed in future - if (r.xaxis && r.yaxis) - plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); - } - - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - function setSelectionPos(pos, e) { - var o = plot.getOptions(); - var offset = plot.getPlaceholder().offset(); - var plotOffset = plot.getPlotOffset(); - pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); - pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); - - if (o.selection.mode == "y") - pos.x = pos == selection.first ? 0 : plot.width(); - - if (o.selection.mode == "x") - pos.y = pos == selection.first ? 0 : plot.height(); - } - - function updateSelection(pos) { - if (pos.pageX == null) - return; - - setSelectionPos(selection.second, pos); - if (selectionIsSane()) { - selection.show = true; - plot.triggerRedrawOverlay(); - } - else - clearSelection(true); - } - - function clearSelection(preventEvent) { - if (selection.show) { - selection.show = false; - plot.triggerRedrawOverlay(); - if (!preventEvent) - plot.getPlaceholder().trigger("plotunselected", [ ]); - } - } - - // function taken from markings support in Flot - function extractRange(ranges, coord) { - var axis, from, to, key, axes = plot.getAxes(); - - for (var k in axes) { - axis = axes[k]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function setSelection(ranges, preventEvent) { - var axis, range, o = plot.getOptions(); - - if (o.selection.mode == "y") { - selection.first.x = 0; - selection.second.x = plot.width(); - } - else { - range = extractRange(ranges, "x"); - - selection.first.x = range.axis.p2c(range.from); - selection.second.x = range.axis.p2c(range.to); - } - - if (o.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plot.height(); - } - else { - range = extractRange(ranges, "y"); - - selection.first.y = range.axis.p2c(range.from); - selection.second.y = range.axis.p2c(range.to); - } - - selection.show = true; - plot.triggerRedrawOverlay(); - if (!preventEvent && selectionIsSane()) - triggerSelectedEvent(); - } - - function selectionIsSane() { - var minSize = plot.getOptions().selection.minSize; - return Math.abs(selection.second.x - selection.first.x) >= minSize && - Math.abs(selection.second.y - selection.first.y) >= minSize; - } - - plot.clearSelection = clearSelection; - plot.setSelection = setSelection; - plot.getSelection = getSelection; - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var o = plot.getOptions(); - if (o.selection.mode != null) { - eventHolder.mousemove(onMouseMove); - eventHolder.mousedown(onMouseDown); - } - }); - - - plot.hooks.drawOverlay.push(function (plot, ctx) { - // draw selection - if (selection.show && selectionIsSane()) { - var plotOffset = plot.getPlotOffset(); - var o = plot.getOptions(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var c = $.color.parse(o.selection.color); - - ctx.strokeStyle = c.scale('a', 0.8).toString(); - ctx.lineWidth = 1; - ctx.lineJoin = o.selection.shape; - ctx.fillStyle = c.scale('a', 0.4).toString(); - - var x = Math.min(selection.first.x, selection.second.x) + 0.5, - y = Math.min(selection.first.y, selection.second.y) + 0.5, - w = Math.abs(selection.second.x - selection.first.x) - 1, - h = Math.abs(selection.second.y - selection.first.y) - 1; - - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - - ctx.restore(); - } - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mousedown", onMouseDown); - - if (mouseUpHandler) - $(document).unbind("mouseup", mouseUpHandler); - }); - - } - - $.plot.plugins.push({ - init: init, - options: { - selection: { - mode: null, // one of null, "x", "y" or "xy" - color: "#e8cfac", - shape: "round", // one of "round", "miter", or "bevel" - minSize: 5 // minimum number of pixels - } - }, - name: 'selection', - version: '1.1' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.stack.js b/src/legacy/ui/public/flot-charts/jquery.flot.stack.js deleted file mode 100644 index 0d91c0f3c0160..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.stack.js +++ /dev/null @@ -1,188 +0,0 @@ -/* Flot plugin for stacking data sets rather than overlaying them. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes the data is sorted on x (or y if stacking horizontally). -For line charts, it is assumed that if a line has an undefined gap (from a -null point), then the line above it should have the same gap - insert zeros -instead of "null" if you want another behaviour. This also holds for the start -and end of the chart. Note that stacking a mix of positive and negative values -in most instances doesn't make sense (so it looks weird). - -Two or more series are stacked when their "stack" attribute is set to the same -key (which can be any number or string or just "true"). To specify the default -stack, you can set the stack option like this: - - series: { - stack: null/false, true, or a key (number/string) - } - -You can also specify it for a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - stack: true - }]) - -The stacking order is determined by the order of the data series in the array -(later series end up on top of the previous). - -Internally, the plugin modifies the datapoints in each series, adding an -offset to the y value. For line series, extra data points are inserted through -interpolation. If there's a second y value, it's also adjusted (e.g for bar -charts or filled areas). - -*/ - -(function ($) { - var options = { - series: { stack: null } // or number/string - }; - - function init(plot) { - function findMatchingSeries(s, allseries) { - var res = null; - for (var i = 0; i < allseries.length; ++i) { - if (s == allseries[i]) - break; - - if (allseries[i].stack == s.stack) - res = allseries[i]; - } - - return res; - } - - function stackData(plot, s, datapoints) { - if (s.stack == null || s.stack === false) - return; - - var other = findMatchingSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - horizontal = s.bars.horizontal, - withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), - withsteps = withlines && s.lines.steps, - fromgap = true, - keyOffset = horizontal ? 1 : 0, - accumulateOffset = horizontal ? 0 : 1, - i = 0, j = 0, l, m; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (points[i] == null) { - // copy gaps - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } - else { - // cases where we actually got two points - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - newpoints[l + accumulateOffset] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - newpoints.push(qx); - newpoints.push(intery + qy); - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - otherps] != null) - bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); - - newpoints[l + accumulateOffset] += bottom; - - i += ps; - } - - fromgap = false; - - if (l != newpoints.length && withbottom) - newpoints[l + 2] += bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(stackData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'stack', - version: '1.2' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.symbol.js b/src/legacy/ui/public/flot-charts/jquery.flot.symbol.js deleted file mode 100644 index 79f634971b6fa..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.symbol.js +++ /dev/null @@ -1,71 +0,0 @@ -/* Flot plugin that adds some extra symbols for plotting points. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The symbols are accessed as strings through the standard symbol options: - - series: { - points: { - symbol: "square" // or "diamond", "triangle", "cross" - } - } - -*/ - -(function ($) { - function processRawData(plot, series, datapoints) { - // we normalize the area of each symbol so it is approximately the - // same as a circle of the given radius - - var handlers = { - square: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.rect(x - size, y - size, size + size, size + size); - }, - diamond: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) - var size = radius * Math.sqrt(Math.PI / 2); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y - size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x - size, y); - }, - triangle: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) - var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); - var height = size * Math.sin(Math.PI / 3); - ctx.moveTo(x - size/2, y + height/2); - ctx.lineTo(x + size/2, y + height/2); - if (!shadow) { - ctx.lineTo(x, y - height/2); - ctx.lineTo(x - size/2, y + height/2); - } - }, - cross: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); - } - }; - - var s = series.points.symbol; - if (handlers[s]) - series.points.symbol = handlers[s]; - } - - function init(plot) { - plot.hooks.processDatapoints.push(processRawData); - } - - $.plot.plugins.push({ - init: init, - name: 'symbols', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.threshold.js b/src/legacy/ui/public/flot-charts/jquery.flot.threshold.js deleted file mode 100644 index 8c99c401d87e5..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.threshold.js +++ /dev/null @@ -1,142 +0,0 @@ -/* Flot plugin for thresholding data. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - series: { - threshold: { - below: number - color: colorspec - } - } - -It can also be applied to a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - threshold: { ... } - }]) - -An array can be passed for multiple thresholding, like this: - - threshold: [{ - below: number1 - color: color1 - },{ - below: number2 - color: color2 - }] - -These multiple threshold objects can be passed in any order since they are -sorted by the processing function. - -The data points below "below" are drawn with the specified color. This makes -it easy to mark points below 0, e.g. for budget data. - -Internally, the plugin works by splitting the data into two series, above and -below the threshold. The extra series below the threshold will have its label -cleared and the special "originSeries" attribute set to the original series. -You may need to check for this in hover events. - -*/ - -(function ($) { - var options = { - series: { threshold: null } // or { below: number, color: color spec} - }; - - function init(plot) { - function thresholdData(plot, s, datapoints, below, color) { - var ps = datapoints.pointsize, i, x, y, p, prevp, - thresholded = $.extend({}, s); // note: shallow copy - - thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format }; - thresholded.label = null; - thresholded.color = color; - thresholded.threshold = null; - thresholded.originSeries = s; - thresholded.data = []; - - var origpoints = datapoints.points, - addCrossingPoints = s.lines.show; - - var threspoints = []; - var newpoints = []; - var m; - - for (i = 0; i < origpoints.length; i += ps) { - x = origpoints[i]; - y = origpoints[i + 1]; - - prevp = p; - if (y < below) - p = threspoints; - else - p = newpoints; - - if (addCrossingPoints && prevp != p && x != null - && i > 0 && origpoints[i - ps] != null) { - var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]); - prevp.push(interx); - prevp.push(below); - for (m = 2; m < ps; ++m) - prevp.push(origpoints[i + m]); - - p.push(null); // start new segment - p.push(null); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - p.push(interx); - p.push(below); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - p.push(x); - p.push(y); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - datapoints.points = newpoints; - thresholded.datapoints.points = threspoints; - - if (thresholded.datapoints.points.length > 0) { - var origIndex = $.inArray(s, plot.getData()); - // Insert newly-generated series right after original one (to prevent it from becoming top-most) - plot.getData().splice(origIndex + 1, 0, thresholded); - } - - // FIXME: there are probably some edge cases left in bars - } - - function processThresholds(plot, s, datapoints) { - if (!s.threshold) - return; - - if (s.threshold instanceof Array) { - s.threshold.sort(function(a, b) { - return a.below - b.below; - }); - - $(s.threshold).each(function(i, th) { - thresholdData(plot, s, datapoints, th.below, th.color); - }); - } - else { - thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color); - } - } - - plot.hooks.processDatapoints.push(processThresholds); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'threshold', - version: '1.2' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.time.js b/src/legacy/ui/public/flot-charts/jquery.flot.time.js deleted file mode 100644 index 7612a03302764..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.time.js +++ /dev/null @@ -1,473 +0,0 @@ -/* Pretty handling of time axes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Set axis.mode to "time" to enable. See the section "Time series data" in -API.txt for details. - -*/ - -import { i18n } from '@kbn/i18n'; - -(function($) { - - var options = { - xaxis: { - timezone: null, // "browser" for local to the client or timezone for timezone-js - timeformat: null, // format string to use - twelveHourClock: false, // 12 or 24 time in time mode - monthNames: null // list of names of months - } - }; - - // round to nearby lower multiple of base - - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - // Returns a string with the date d formatted according to fmt. - // A subset of the Open Group's strftime format is supported. - - function formatDate(d, fmt, monthNames, dayNames) { - - if (typeof d.strftime == "function") { - return d.strftime(fmt); - } - - var leftPad = function(n, pad) { - n = "" + n; - pad = "" + (pad == null ? "0" : pad); - return n.length == 1 ? pad + n : n; - }; - - var r = []; - var escape = false; - var hours = d.getHours(); - var isAM = hours < 12; - - if (monthNames == null) { - monthNames = [ - i18n.translate('common.ui.flotCharts.janLabel', { - defaultMessage: 'Jan', - }), i18n.translate('common.ui.flotCharts.febLabel', { - defaultMessage: 'Feb', - }), i18n.translate('common.ui.flotCharts.marLabel', { - defaultMessage: 'Mar', - }), i18n.translate('common.ui.flotCharts.aprLabel', { - defaultMessage: 'Apr', - }), i18n.translate('common.ui.flotCharts.mayLabel', { - defaultMessage: 'May', - }), i18n.translate('common.ui.flotCharts.junLabel', { - defaultMessage: 'Jun', - }), i18n.translate('common.ui.flotCharts.julLabel', { - defaultMessage: 'Jul', - }), i18n.translate('common.ui.flotCharts.augLabel', { - defaultMessage: 'Aug', - }), i18n.translate('common.ui.flotCharts.sepLabel', { - defaultMessage: 'Sep', - }), i18n.translate('common.ui.flotCharts.octLabel', { - defaultMessage: 'Oct', - }), i18n.translate('common.ui.flotCharts.novLabel', { - defaultMessage: 'Nov', - }), i18n.translate('common.ui.flotCharts.decLabel', { - defaultMessage: 'Dec', - })]; - } - - if (dayNames == null) { - dayNames = [i18n.translate('common.ui.flotCharts.sunLabel', { - defaultMessage: 'Sun', - }), i18n.translate('common.ui.flotCharts.monLabel', { - defaultMessage: 'Mon', - }), i18n.translate('common.ui.flotCharts.tueLabel', { - defaultMessage: 'Tue', - }), i18n.translate('common.ui.flotCharts.wedLabel', { - defaultMessage: 'Wed', - }), i18n.translate('common.ui.flotCharts.thuLabel', { - defaultMessage: 'Thu', - }), i18n.translate('common.ui.flotCharts.friLabel', { - defaultMessage: 'Fri', - }), i18n.translate('common.ui.flotCharts.satLabel', { - defaultMessage: 'Sat', - })]; - } - - var hours12; - - if (hours > 12) { - hours12 = hours - 12; - } else if (hours == 0) { - hours12 = 12; - } else { - hours12 = hours; - } - - for (var i = 0; i < fmt.length; ++i) { - - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'a': c = "" + dayNames[d.getDay()]; break; - case 'b': c = "" + monthNames[d.getMonth()]; break; - case 'd': c = leftPad(d.getDate()); break; - case 'e': c = leftPad(d.getDate(), " "); break; - case 'h': // For back-compat with 0.7; remove in 1.0 - case 'H': c = leftPad(hours); break; - case 'I': c = leftPad(hours12); break; - case 'l': c = leftPad(hours12, " "); break; - case 'm': c = leftPad(d.getMonth() + 1); break; - case 'M': c = leftPad(d.getMinutes()); break; - // quarters not in Open Group's strftime specification - case 'q': - c = "" + (Math.floor(d.getMonth() / 3) + 1); break; - case 'S': c = leftPad(d.getSeconds()); break; - case 'y': c = leftPad(d.getFullYear() % 100); break; - case 'Y': c = "" + d.getFullYear(); break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case 'w': c = "" + d.getDay(); break; - } - r.push(c); - escape = false; - } else { - if (c == "%") { - escape = true; - } else { - r.push(c); - } - } - } - - return r.join(""); - } - - // To have a consistent view of time-based data independent of which time - // zone the client happens to be in we need a date-like object independent - // of time zones. This is done through a wrapper that only calls the UTC - // versions of the accessor methods. - - function makeUtcWrapper(d) { - - function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { - sourceObj[sourceMethod] = function() { - return targetObj[targetMethod].apply(targetObj, arguments); - }; - }; - - var utc = { - date: d - }; - - // support strftime, if found - - if (d.strftime != undefined) { - addProxyMethod(utc, "strftime", d, "strftime"); - } - - addProxyMethod(utc, "getTime", d, "getTime"); - addProxyMethod(utc, "setTime", d, "setTime"); - - var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; - - for (var p = 0; p < props.length; p++) { - addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); - addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); - } - - return utc; - }; - - // select time zone strategy. This returns a date-like object tied to the - // desired timezone - - function dateGenerator(ts, opts) { - if (opts.timezone == "browser") { - return new Date(ts); - } else if (!opts.timezone || opts.timezone == "utc") { - return makeUtcWrapper(new Date(ts)); - } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { - var d = new timezoneJS.Date(); - // timezone-js is fickle, so be sure to set the time zone before - // setting the time. - d.setTimezone(opts.timezone); - d.setTime(ts); - return d; - } else { - return makeUtcWrapper(new Date(ts)); - } - } - - // map of app. size of time units in milliseconds - - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "quarter": 3 * 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - - var baseSpec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"] - ]; - - // we don't know which variant(s) we'll need yet, but generating both is - // cheap - - var specMonths = baseSpec.concat([[3, "month"], [6, "month"], - [1, "year"]]); - var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], - [1, "year"]]); - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode == "time") { - axis.tickGenerator = function(axis) { - - var ticks = []; - var d = dateGenerator(axis.min, opts); - var minSize = 0; - - // make quarter use a possibility if quarters are - // mentioned in either of these options - - var spec = (opts.tickSize && opts.tickSize[1] === - "quarter") || - (opts.minTickSize && opts.minTickSize[1] === - "quarter") ? specQuarters : specMonths; - - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") { - minSize = opts.tickSize; - } else { - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - } - - for (var i = 0; i < spec.length - 1; ++i) { - if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { - break; - } - } - - var size = spec[i][0]; - var unit = spec[i][1]; - - // special-case the possibility of several years - - if (unit == "year") { - - // if given a minTickSize in years, just use it, - // ensuring that it's an integer - - if (opts.minTickSize != null && opts.minTickSize[1] == "year") { - size = Math.floor(opts.minTickSize[0]); - } else { - - var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); - var norm = (axis.delta / timeUnitSize.year) / magn; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - } - - // minimum size for years is 1 - - if (size < 1) { - size = 1; - } - } - - axis.tickSize = opts.tickSize || [size, unit]; - var tickSize = axis.tickSize[0]; - unit = axis.tickSize[1]; - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") { - d.setSeconds(floorInBase(d.getSeconds(), tickSize)); - } else if (unit == "minute") { - d.setMinutes(floorInBase(d.getMinutes(), tickSize)); - } else if (unit == "hour") { - d.setHours(floorInBase(d.getHours(), tickSize)); - } else if (unit == "month") { - d.setMonth(floorInBase(d.getMonth(), tickSize)); - } else if (unit == "quarter") { - d.setMonth(3 * floorInBase(d.getMonth() / 3, - tickSize)); - } else if (unit == "year") { - d.setFullYear(floorInBase(d.getFullYear(), tickSize)); - } - - // reset smaller components - - d.setMilliseconds(0); - - if (step >= timeUnitSize.minute) { - d.setSeconds(0); - } - if (step >= timeUnitSize.hour) { - d.setMinutes(0); - } - if (step >= timeUnitSize.day) { - d.setHours(0); - } - if (step >= timeUnitSize.day * 4) { - d.setDate(1); - } - if (step >= timeUnitSize.month * 2) { - d.setMonth(floorInBase(d.getMonth(), 3)); - } - if (step >= timeUnitSize.quarter * 2) { - d.setMonth(floorInBase(d.getMonth(), 6)); - } - if (step >= timeUnitSize.year) { - d.setMonth(0); - } - - var carry = 0; - var v = Number.NaN; - var prev; - - do { - - prev = v; - v = d.getTime(); - ticks.push(v); - - if (unit == "month" || unit == "quarter") { - if (tickSize < 1) { - - // a bit complicated - we'll divide the - // month/quarter up but we need to take - // care of fractions so we don't end up in - // the middle of a day - - d.setDate(1); - var start = d.getTime(); - d.setMonth(d.getMonth() + - (unit == "quarter" ? 3 : 1)); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getHours(); - d.setHours(0); - } else { - d.setMonth(d.getMonth() + - tickSize * (unit == "quarter" ? 3 : 1)); - } - } else if (unit == "year") { - d.setFullYear(d.getFullYear() + tickSize); - } else { - d.setTime(v + step); - } - } while (v < axis.max && v != prev); - - return ticks; - }; - - axis.tickFormatter = function (v, axis) { - - var d = dateGenerator(v, axis.options); - - // first check global format - - if (opts.timeformat != null) { - return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); - } - - // possibly use quarters if quarters are mentioned in - // any of these places - - var useQuarters = (axis.options.tickSize && - axis.options.tickSize[1] == "quarter") || - (axis.options.minTickSize && - axis.options.minTickSize[1] == "quarter"); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; - var fmt; - - if (t < timeUnitSize.minute) { - fmt = hourCode + ":%M:%S" + suffix; - } else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) { - fmt = hourCode + ":%M" + suffix; - } else { - fmt = "%b %d " + hourCode + ":%M" + suffix; - } - } else if (t < timeUnitSize.month) { - fmt = "%b %d"; - } else if ((useQuarters && t < timeUnitSize.quarter) || - (!useQuarters && t < timeUnitSize.year)) { - if (span < timeUnitSize.year) { - fmt = "%b"; - } else { - fmt = "%b %Y"; - } - } else if (useQuarters && t < timeUnitSize.year) { - if (span < timeUnitSize.year) { - fmt = "Q%q"; - } else { - fmt = "Q%q %Y"; - } - } else { - fmt = "%Y"; - } - - var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); - - return rt; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'time', - version: '1.0' - }); - - // Time-axis support used to be in Flot core, which exposed the - // formatDate function on the plot object. Various plugins depend - // on the function, so we need to re-expose it here. - - $.plot.formatDate = formatDate; - $.plot.dateGenerator = dateGenerator; - -})(jQuery); diff --git a/src/legacy/ui/public/i18n/__snapshots__/index.test.tsx.snap b/src/legacy/ui/public/i18n/__snapshots__/index.test.tsx.snap deleted file mode 100644 index fd6a0a07ba39c..0000000000000 --- a/src/legacy/ui/public/i18n/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ui/i18n renders children and forwards properties 1`] = ` -
- Context: - - Child: some prop:100500 - -
-`; diff --git a/src/legacy/ui/public/i18n/index.test.tsx b/src/legacy/ui/public/i18n/index.test.tsx deleted file mode 100644 index be8ab4cf8d696..0000000000000 --- a/src/legacy/ui/public/i18n/index.test.tsx +++ /dev/null @@ -1,61 +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 { render } from 'enzyme'; -import PropTypes from 'prop-types'; -import React from 'react'; - -jest.mock('angular-sanitize', () => {}); -jest.mock('ui/new_platform', () => ({ - npStart: { - core: { - i18n: { Context: ({ children }: any) =>
Context: {children}
}, - }, - }, -})); - -import { wrapInI18nContext } from '.'; - -describe('ui/i18n', () => { - test('renders children and forwards properties', () => { - const mockPropTypes = { - stringProp: PropTypes.string.isRequired, - numberProp: PropTypes.number, - }; - - const WrappedComponent = wrapInI18nContext( - class extends React.PureComponent<{ [P in keyof typeof mockPropTypes]: unknown }> { - public static propTypes = mockPropTypes; - - public render() { - return ( - - Child: {this.props.stringProp}:{this.props.numberProp} - - ); - } - } - ); - - expect(WrappedComponent.propTypes).toBe(mockPropTypes); - expect( - render() - ).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx deleted file mode 100644 index 290e82a1334b9..0000000000000 --- a/src/legacy/ui/public/i18n/index.tsx +++ /dev/null @@ -1,52 +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 React from 'react'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; - -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; - -export const I18nContext = npStart.core.i18n.Context; - -export function wrapInI18nContext

(ComponentToWrap: React.ComponentType

) { - const ContextWrapper: React.FC

= (props) => { - return ( - - - - ); - }; - - // Original propTypes from the wrapped component should be re-exposed - // since it will be used by reactDirective Angular service - // that will rely on propTypes to watch attributes with these names - ContextWrapper.propTypes = ComponentToWrap.propTypes; - - return ContextWrapper; -} - -uiModules - .get('i18n', ['ngSanitize']) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); diff --git a/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js b/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js deleted file mode 100644 index df96a58a6e99f..0000000000000 --- a/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js +++ /dev/null @@ -1,208 +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 _ from 'lodash'; -import expect from '@kbn/expect'; -import { IndexedArray } from '..'; - -// this is generally a data-structure that IndexedArray is good for managing -const users = [ - { name: 'John', id: 6, username: 'beast', group: 'admins' }, - { name: 'Anon', id: 0, username: 'shhhh', group: 'secret' }, - { name: 'Fern', id: 42, username: 'kitty', group: 'editor' }, - { name: 'Mary', id: 55, username: 'sheep', group: 'editor' }, -]; - -// this is how we used to accomplish this, before IndexedArray -users.byName = _.keyBy(users, 'name'); -users.byUsername = _.keyBy(users, 'username'); -users.byGroup = _.groupBy(users, 'group'); -users.inIdOrder = _.sortBy(users, 'id'); - -// then things started becoming unruly... so IndexedArray! - -describe('IndexedArray', function () { - describe('Basics', function () { - let reg; - - beforeEach(function () { - reg = new IndexedArray(); - }); - - it('Extends Array', function () { - expect(reg).to.be.a(Array); - }); - - it('fails basic lodash check', function () { - expect(Array.isArray(reg)).to.be(false); - }); - - it('clones to an object', function () { - expect(_.isObject(_.clone(reg))).to.be(true); - expect(Array.isArray(_.clone(reg))).to.be(false); - }); - }); - - describe('Indexing', function () { - it('provides the initial set', function () { - const reg = new IndexedArray({ - initialSet: [1, 2, 3], - }); - - expect(reg).to.have.length(3); - - reg.forEach(function (v, i) { - expect(v).to.eql(i + 1); - }); - }); - - it('indexes the initial set', function () { - const reg = new IndexedArray({ - index: ['username'], - initialSet: users, - }); - - expect(reg).to.have.property('byUsername'); - expect(reg.byUsername).to.eql(users.byUsername); - }); - - it('updates indices after values are added', function () { - // split up the user list, and add it in chunks - const firstUser = users.slice(0, 1).pop(); - const otherUsers = users.slice(1); - - // start off with all but the first - const reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: otherUsers, - }); - - // add the first - reg.push(firstUser); - - // end up with the same structure that is in the users fixture - expect(Object.keys(reg.byGroup).length).to.be(Object.keys(users.byGroup).length); - for (const group of Object.keys(reg.byGroup)) { - expect(reg.byGroup[group].toJSON()).to.eql(users.byGroup[group]); - } - - expect(reg.inIdOrder).to.eql(users.inIdOrder); - }); - - it('updates indices after values are removed', function () { - // start off with all - const reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: users, - }); - - // remove the last - reg.pop(); - - const expectedCount = users.length - 1; - // indexed lists should be updated - expect(reg).to.have.length(expectedCount); - - const sumOfGroups = _.reduce( - reg.byGroup, - function (note, group) { - return note + group.length; - }, - 0 - ); - expect(sumOfGroups).to.eql(expectedCount); - }); - - it('removes items based on a predicate', function () { - const reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: users, - }); - - reg.remove({ name: 'John' }); - - expect(_.isEqual(reg.raw, reg.slice(0))).to.be(true); - expect(reg.length).to.be(3); - expect(reg[0].name).to.be('Anon'); - }); - - it('updates indices after values are re-ordered', function () { - const rawUsers = users.slice(0); - - // collect and shuffle the ids available - let ids = []; - _.times(rawUsers.length, function (i) { - ids.push(i); - }); - ids = _.shuffle(ids); - - // move something here - const toI = ids.shift(); - // from here - const fromI = ids.shift(); - // do the move - const move = function (arr) { - arr.splice(toI, 0, arr.splice(fromI, 1)[0]); - }; - - const reg = new IndexedArray({ - index: ['username'], - initialSet: rawUsers, - }); - - const index = reg.byUsername; - - move(reg); - - expect(reg.byUsername).to.eql(index); - expect(reg.byUsername).to.not.be(index); - }); - }); - - describe('Ordering', function () { - it('ordering is case insensitive', function () { - const reg = new IndexedArray({ - index: ['title'], - order: ['title'], - initialSet: [{ title: 'APM' }, { title: 'Advanced Settings' }], - }); - - const ordered = reg.inTitleOrder; - expect(ordered[0].title).to.be('Advanced Settings'); - expect(ordered[1].title).to.be('APM'); - }); - - it('ordering handles numbers', function () { - const reg = new IndexedArray({ - index: ['id'], - order: ['id'], - initialSet: users, - }); - - const ordered = reg.inIdOrder; - expect(ordered[0].id).to.be(0); - expect(ordered[1].id).to.be(6); - expect(ordered[2].id).to.be(42); - expect(ordered[3].id).to.be(55); - }); - }); -}); diff --git a/src/legacy/ui/public/indexed_array/__tests__/inflector.js b/src/legacy/ui/public/indexed_array/__tests__/inflector.js deleted file mode 100644 index 49ac79094e501..0000000000000 --- a/src/legacy/ui/public/indexed_array/__tests__/inflector.js +++ /dev/null @@ -1,63 +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 { inflector } from '../inflector'; -import expect from '@kbn/expect'; - -describe('IndexedArray Inflector', function () { - it('returns a function', function () { - const getter = inflector(); - expect(getter).to.be.a('function'); - }); - - describe('fn', function () { - it('prepends a prefix', function () { - const inflect = inflector('my'); - - expect(inflect('Family')).to.be('myFamily'); - expect(inflect('family')).to.be('myFamily'); - expect(inflect('fAmIlY')).to.be('myFAmIlY'); - }); - - it('adds both a prefix and suffix', function () { - const inflect = inflector('foo', 'Bar'); - - expect(inflect('box')).to.be('fooBoxBar'); - expect(inflect('box.car.MAX')).to.be('fooBoxCarMaxBar'); - expect(inflect('BaZzY')).to.be('fooBaZzYBar'); - }); - - it('ignores prefix if it is already at the end of the inflected string', function () { - const inflect = inflector('foo', 'Bar'); - expect(inflect('fooBox')).to.be('fooBoxBar'); - expect(inflect('FooBox')).to.be('FooBoxBar'); - }); - - it('ignores postfix if it is already at the end of the inflected string', function () { - const inflect = inflector('foo', 'Bar'); - expect(inflect('bar')).to.be('fooBar'); - expect(inflect('showBoxBar')).to.be('fooShowBoxBar'); - }); - - it('works with "name"', function () { - const inflect = inflector('in', 'Order'); - expect(inflect('name')).to.be('inNameOrder'); - }); - }); -}); diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts deleted file mode 100644 index fc4ca8469382a..0000000000000 --- a/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts +++ /dev/null @@ -1,63 +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 { groupBy } from 'lodash'; -import { organizeBy } from './organize_by'; - -describe('organizeBy', () => { - test('it works', () => { - const col = [ - { - name: 'one', - roles: ['user', 'admin', 'owner'], - }, - { - name: 'two', - roles: ['user'], - }, - { - name: 'three', - roles: ['user'], - }, - { - name: 'four', - roles: ['user', 'admin'], - }, - ]; - - const resp = organizeBy(col, 'roles'); - expect(resp).toHaveProperty('user'); - expect(resp.user.length).toBe(4); - - expect(resp).toHaveProperty('admin'); - expect(resp.admin.length).toBe(2); - - expect(resp).toHaveProperty('owner'); - expect(resp.owner.length).toBe(1); - }); - - test('behaves just like groupBy in normal scenarios', () => { - const col = [{ name: 'one' }, { name: 'two' }, { name: 'three' }, { name: 'four' }]; - - const orgs = organizeBy(col, 'name'); - const groups = groupBy(col, 'name'); - - expect(orgs).toEqual(groups); - }); -}); diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.ts deleted file mode 100644 index e923767c892cd..0000000000000 --- a/src/legacy/ui/public/indexed_array/helpers/organize_by.ts +++ /dev/null @@ -1,61 +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 { each, isFunction } from 'lodash'; - -/** - * Like _.groupBy, but allows specifying multiple groups for a - * single object. - * - * organizeBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') - * // Object {1: Array[2], 2: Array[1], 3: Array[1], 4: Array[1]} - * - * _.groupBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') - * // Object {'1,2,3': Array[1], '1,4': Array[1]} - * - * @param {array} collection - the list of values to organize - * @param {Function} callback - either a property name, or a callback. - * @return {object} - */ -export function organizeBy(collection: object[], callback: ((obj: object) => string) | string) { - const buckets: { [key: string]: object[] } = {}; - - function add(key: string, obj: object) { - if (!buckets[key]) { - buckets[key] = []; - } - buckets[key].push(obj); - } - - each(collection, (obj: Record) => { - const keys = isFunction(callback) ? callback(obj) : obj[callback]; - - if (!Array.isArray(keys)) { - add(keys, obj); - return; - } - - let length = keys.length; - while (length-- > 0) { - add(keys[length], obj); - } - }); - - return buckets; -} diff --git a/src/legacy/ui/public/indexed_array/index.d.ts b/src/legacy/ui/public/indexed_array/index.d.ts deleted file mode 100644 index 21c0a818731ac..0000000000000 --- a/src/legacy/ui/public/indexed_array/index.d.ts +++ /dev/null @@ -1,44 +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 { ListIterator } from 'lodash'; - -interface IndexedArrayConfig { - index?: string[]; - group?: string[]; - order?: string[]; - initialSet?: T[]; - immutable?: boolean; -} - -declare class IndexedArray extends Array { - public immutable: boolean; - public raw: T[]; - - // These may not actually be present, as they are dynamically defined - public inOrder: T[]; - public byType: Record; - public byName: Record; - - constructor(config: IndexedArrayConfig); - - public remove(predicate: ListIterator): T[]; - - public toJSON(): T[]; -} diff --git a/src/legacy/ui/public/indexed_array/index.js b/src/legacy/ui/public/indexed_array/index.js deleted file mode 100644 index 6a42961c9e680..0000000000000 --- a/src/legacy/ui/public/indexed_array/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 { IndexedArray } from './indexed_array'; diff --git a/src/legacy/ui/public/indexed_array/indexed_array.js b/src/legacy/ui/public/indexed_array/indexed_array.js deleted file mode 100644 index b9a427b8da7ad..0000000000000 --- a/src/legacy/ui/public/indexed_array/indexed_array.js +++ /dev/null @@ -1,235 +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 _ from 'lodash'; -import { inflector } from './inflector'; -import { organizeBy } from './helpers/organize_by'; - -const pathGetter = _(_.get).rearg(1, 0).ary(2); -const inflectIndex = inflector('by'); -const inflectOrder = inflector('in', 'Order'); - -const CLEAR_CACHE = {}; -const OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable']; - -/** - * Generic extension of Array class, which will index (and reindex) the - * objects it contains based on their properties. - * - * @param {Object} config describes the properties of this registry object - * @param {Array} [config.index] a list of props/paths that should be used to index the docs. - * @param {Array} [config.group] a list of keys/paths to group docs by. - * @param {Array} [config.order] a list of keys/paths to order the keys by. - * @param {Array} [config.initialSet] the initial dataset the IndexedArray should contain. - * @param {boolean} [config.immutable] a flag that hints to people reading the implementation that this IndexedArray - * should not be modified - */ - -export class IndexedArray { - static OPT_NAMES = OPT_NAMES; - - constructor(config) { - config = _.pick(config || {}, OPT_NAMES); - - // use defineProperty so that value can't be changed - Object.defineProperty(this, 'raw', { value: [] }); - - this._indexNames = _.union( - this._setupIndex(config.group, inflectIndex, organizeByIndexedArray(config)), - this._setupIndex(config.index, inflectIndex, _.keyBy), - this._setupIndex(config.order, inflectOrder, (raw, pluckValue) => { - return [...raw].sort((itemA, itemB) => { - const a = pluckValue(itemA); - const b = pluckValue(itemB); - if (typeof a === 'number' && typeof b === 'number') { - return a - b; - } - return String(a).toLowerCase().localeCompare(String(b).toLowerCase()); - }); - }) - ); - - if (config.initialSet) { - this.push.apply(this, config.initialSet); - } - - Object.defineProperty(this, 'immutable', { value: !!config.immutable }); - } - - /** - * Remove items from this based on a predicate - * @param {Function|Object|string} predicate - the predicate used to decide what is removed - * @return {array} - the removed data - */ - remove(predicate) { - this._assertMutable('remove'); - const out = _.remove(this, predicate); - _.remove(this.raw, predicate); - this._clearIndices(); - return out; - } - - /** - * provide a hook for the JSON serializer - * @return {array} - a plain, vanilla array with our same data - */ - toJSON() { - return this.raw; - } - - // wrappers for mutable Array methods - copyWithin(...args) { - return this._mutation('copyWithin', args); - } - fill(...args) { - return this._mutation('fill', args); - } - pop(...args) { - return this._mutation('pop', args); - } - push(...args) { - return this._mutation('push', args); - } - reverse(...args) { - return this._mutation('reverse', args); - } - shift(...args) { - return this._mutation('shift', args); - } - sort(...args) { - return this._mutation('sort', args); - } - splice(...args) { - return this._mutation('splice', args); - } - unshift(...args) { - return this._mutation('unshift', args); - } - - /** - * If this instance of IndexedArray is not mutable, throw an error - * @private - * @param {String} methodName - user facing method name, for error message - * @return {undefined} - */ - _assertMutable(methodName) { - if (this.immutable) { - throw new Error(`${methodName}() is not allowed on immutable IndexedArray instances`); - } - } - - /** - * Execute some mutable method from the Array prototype - * on the IndexedArray and this.raw - * - * @private - * @param {string} methodName - * @param {Array} args - * @return {any} - */ - _mutation(methodName, args) { - this._assertMutable(methodName); - super[methodName].apply(this, args); - this._clearIndices(); - return super[methodName].apply(this.raw, args); - } - - /** - * Create indices for a group of object properties. getters and setters are used to - * read and control the indices. - * @private - * @param {string[]} props - the properties that should be used to index docs - * @param {function} inflect - a function that will be called with a property name, and - * creates the public property at which the index will be exposed - * @param {function} op - the function that will be used to create the indices, it is passed - * the raw representation of the registry, and a getter for reading the - * right prop - * - * @returns {string[]} - the public keys of all indices created - */ - _setupIndex(props, inflect, op) { - // shortcut for empty props - if (!props || props.length === 0) return; - - return props.map((prop) => { - const indexName = inflect(prop); - const getIndexValueFromItem = pathGetter.partial(prop).value(); - let cache; - - Object.defineProperty(this, indexName, { - enumerable: false, - configurable: false, - - set: (val) => { - // can't set any value other than the CLEAR_CACHE constant - if (val === CLEAR_CACHE) { - cache = false; - } else { - throw new TypeError(indexName + ' can not be set, it is a computed index of values'); - } - }, - get: () => { - if (!cache) { - cache = op(this.raw, getIndexValueFromItem); - } - - return cache; - }, - }); - - return indexName; - }); - } - - /** - * Clear cached index/group/order caches so they will be recreated - * on next access - * @private - * @return {undefined} - */ - _clearIndices() { - this._indexNames.forEach((name) => { - this[name] = CLEAR_CACHE; - }); - } -} - -// using traditional `extends Array` syntax doesn't work with babel -// See https://babeljs.io/docs/usage/caveats/ -Object.setPrototypeOf(IndexedArray.prototype, Array.prototype); - -// Similar to `organizeBy` but returns IndexedArrays instead of normal Arrays. -function organizeByIndexedArray(config) { - return (...args) => { - const grouped = organizeBy(...args); - - return _.reduce( - grouped, - (acc, value, group) => { - acc[group] = new IndexedArray({ - ...config, - initialSet: value, - }); - - return acc; - }, - {} - ); - }; -} diff --git a/src/legacy/ui/public/indexed_array/inflector.js b/src/legacy/ui/public/indexed_array/inflector.js deleted file mode 100644 index e034146f5f62f..0000000000000 --- a/src/legacy/ui/public/indexed_array/inflector.js +++ /dev/null @@ -1,60 +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. - */ - -function upFirst(str, total) { - return str.charAt(0).toUpperCase() + (total ? str.substr(1).toLowerCase() : str.substr(1)); -} - -function startsWith(str, test) { - return str.substr(0, test.length).toLowerCase() === test.toLowerCase(); -} - -function endsWith(str, test) { - const tooShort = str.length < test.length; - if (tooShort) return; - - return str.substr(str.length - test.length).toLowerCase() === test.toLowerCase(); -} - -export function inflector(prefix, postfix) { - return function inflect(key) { - let inflected; - - if (key.indexOf('.') !== -1) { - inflected = key - .split('.') - .map(function (step, i) { - return i === 0 ? step : upFirst(step, true); - }) - .join(''); - } else { - inflected = key; - } - - if (prefix && !startsWith(key, prefix)) { - inflected = prefix + upFirst(inflected); - } - - if (postfix && !endsWith(key, postfix)) { - inflected = inflected + postfix; - } - - return inflected; - }; -} diff --git a/src/legacy/ui/public/kfetch/__mocks__/index.ts b/src/legacy/ui/public/kfetch/__mocks__/index.ts deleted file mode 100644 index 1a128e2b85260..0000000000000 --- a/src/legacy/ui/public/kfetch/__mocks__/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 const kfetch = () => Promise.resolve(); diff --git a/src/legacy/ui/public/kfetch/_import_objects.ndjson b/src/legacy/ui/public/kfetch/_import_objects.ndjson deleted file mode 100644 index 3511fb44cdfb2..0000000000000 --- a/src/legacy/ui/public/kfetch/_import_objects.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1} diff --git a/src/legacy/ui/public/kfetch/index.ts b/src/legacy/ui/public/kfetch/index.ts deleted file mode 100644 index 105df171ad370..0000000000000 --- a/src/legacy/ui/public/kfetch/index.ts +++ /dev/null @@ -1,28 +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 { npSetup } from 'ui/new_platform'; -import { createKfetch, KFetchKibanaOptions, KFetchOptions } from './kfetch'; -export { addInterceptor, KFetchOptions, KFetchQuery } from './kfetch'; - -const kfetchInstance = createKfetch(npSetup.core.http); - -export const kfetch = (options: KFetchOptions, kfetchOptions?: KFetchKibanaOptions) => { - return kfetchInstance(options, kfetchOptions); -}; diff --git a/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts b/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts deleted file mode 100644 index ea066b3623f13..0000000000000 --- a/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts +++ /dev/null @@ -1,26 +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 { setup } from '../../../../test_utils/public/http_test_setup'; - -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: setup(), - }, -})); diff --git a/src/legacy/ui/public/kfetch/kfetch.test.ts b/src/legacy/ui/public/kfetch/kfetch.test.ts deleted file mode 100644 index c45a142d54e9b..0000000000000 --- a/src/legacy/ui/public/kfetch/kfetch.test.ts +++ /dev/null @@ -1,487 +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 fetchMock from 'fetch-mock/es5/client'; -import './kfetch.test.mocks'; -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { addInterceptor, kfetch, KFetchOptions } from '.'; -import { Interceptor, resetInterceptors, withDefaultOptions } from './kfetch'; -import { KFetchError } from './kfetch_error'; - -describe('kfetch', () => { - afterEach(() => { - fetchMock.restore(); - resetInterceptors(); - }); - - it('should use supplied request method', async () => { - fetchMock.post('*', {}); - await kfetch({ pathname: '/my/path', method: 'POST' }); - expect(fetchMock.lastOptions()!.method).toBe('POST'); - }); - - it('should use supplied Content-Type', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path', headers: { 'Content-Type': 'CustomContentType' } }); - expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'content-type': 'CustomContentType', - }); - }); - - it('should use supplied pathname and querystring', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path', query: { a: 'b' } }); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); - }); - - it('should use supplied headers', async () => { - fetchMock.get('*', {}); - await kfetch({ - pathname: '/my/path', - headers: { myHeader: 'foo' }, - }); - - expect(fetchMock.lastOptions()!.headers).toEqual({ - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - myheader: 'foo', - }); - }); - - it('should return response', async () => { - fetchMock.get('*', { foo: 'bar' }); - const res = await kfetch({ pathname: '/my/path' }); - expect(res).toEqual({ foo: 'bar' }); - }); - - it('should prepend url with basepath by default', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path' }); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); - }); - - it('should not prepend url with basepath when disabled', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path' }, { prependBasePath: false }); - expect(fetchMock.lastUrl()).toBe('/my/path'); - }); - - it('should make request with defaults', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path' }); - - expect(fetchMock.lastCall()!.request.credentials).toBe('same-origin'); - expect(fetchMock.lastOptions()!).toMatchObject({ - method: 'GET', - headers: { - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - }, - }); - }); - - it('should make requests for NDJSON content', async () => { - const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); - - fetchMock.post('*', { - body: content, - headers: { 'Content-Type': 'application/ndjson' }, - }); - - const data = await kfetch({ - method: 'POST', - pathname: '/my/path', - body: content, - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - - expect(data).toBeInstanceOf(Blob); - - const ndjson = await new Response(data).text(); - - expect(ndjson).toEqual(content); - }); - - it('should reject on network error', async () => { - expect.assertions(1); - fetchMock.get('*', { status: 500 }); - - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - expect(e.message).toBe('Internal Server Error'); - } - }); - - describe('when throwing response error (KFetchError)', () => { - let error: KFetchError; - beforeEach(async () => { - fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - error = e; - } - }); - - it('should contain error message', () => { - expect(error.message).toBe('Not Found'); - }); - - it('should return response body', () => { - expect(error.body).toEqual({ foo: 'bar' }); - }); - - it('should contain response properties', () => { - expect(error.res.status).toBe(404); - expect(error.res.url).toBe('http://localhost/myBase/my/path'); - }); - }); - - describe('when all interceptor resolves', () => { - let resp: any; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([{}, {}, {}]); - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should call interceptors in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'Request #1', - 'Response #1', - 'Response #2', - 'Response #3', - ]); - }); - - it('should make request', () => { - expect(fetchMock.called()).toBe(true); - }); - - it('should return response', () => { - expect(resp).toEqual({ foo: 'bar' }); - }); - }); - - describe('when a request interceptor throws; and the next requestError interceptor resolves', () => { - let resp: any; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - { requestError: () => ({ pathname: '/my/path' } as KFetchOptions) }, - { request: () => Promise.reject(new Error('Error in request')) }, - {}, - ]); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should call interceptors in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'RequestError #1', - 'Response #1', - 'Response #2', - 'Response #3', - ]); - }); - - it('should make request', () => { - expect(fetchMock.called()).toBe(true); - }); - - it('should return response', () => { - expect(resp).toEqual({ foo: 'bar' }); - }); - }); - - describe('when a request interceptor throws', () => { - let error: Error; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - {}, - { request: () => Promise.reject(new Error('Error in request')) }, - {}, - ]); - - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - error = e; - } - }); - - it('should call interceptors in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'RequestError #1', - 'ResponseError #1', - 'ResponseError #2', - 'ResponseError #3', - ]); - }); - - it('should not make request', () => { - expect(fetchMock.called()).toBe(false); - }); - - it('should throw error', () => { - expect(error.message).toEqual('Error in request'); - }); - }); - - describe('when a response interceptor throws', () => { - let error: Error; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - { response: () => Promise.reject(new Error('Error in response')) }, - {}, - {}, - ]); - - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - error = e; - } - }); - - it('should call in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'Request #1', - 'Response #1', - 'ResponseError #2', - 'ResponseError #3', - ]); - }); - - it('should make request', () => { - expect(fetchMock.called()).toBe(true); - }); - - it('should throw error', () => { - expect(error.message).toEqual('Error in response'); - }); - }); - - describe('when request interceptor throws; and a responseError interceptor resolves', () => { - let resp: any; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - {}, - { - request: () => { - throw new Error('My request error'); - }, - responseError: () => { - return { custom: 'response' }; - }, - }, - {}, - ]); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should call in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'RequestError #1', - 'ResponseError #1', - 'ResponseError #2', - 'Response #3', - ]); - }); - - it('should not make request', () => { - expect(fetchMock.called()).toBe(false); - }); - - it('should resolve', () => { - expect(resp).toEqual({ custom: 'response' }); - }); - }); - - describe('when interceptors return synchronously', () => { - let resp: any; - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - addInterceptor({ - request: (config) => ({ - ...config, - pathname: '/my/intercepted-route', - }), - response: (res) => ({ - ...res, - addedByResponseInterceptor: true, - }), - }); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should modify request', () => { - expect(fetchMock.lastUrl()).toContain('/my/intercepted-route'); - expect(fetchMock.lastOptions()!).toMatchObject({ - method: 'GET', - }); - }); - - it('should modify response', () => { - expect(resp).toEqual({ - addedByResponseInterceptor: true, - foo: 'bar', - }); - }); - }); - - describe('when interceptors return promise', () => { - let resp: any; - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - addInterceptor({ - request: (config) => - Promise.resolve({ - ...config, - pathname: '/my/intercepted-route', - }), - response: (res) => - Promise.resolve({ - ...res, - addedByResponseInterceptor: true, - }), - }); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should modify request', () => { - expect(fetchMock.lastUrl()).toContain('/my/intercepted-route'); - expect(fetchMock.lastOptions()!).toMatchObject({ - method: 'GET', - }); - }); - - it('should modify response', () => { - expect(resp).toEqual({ - addedByResponseInterceptor: true, - foo: 'bar', - }); - }); - }); -}); - -function mockInterceptorCalls(interceptors: Interceptor[]) { - const interceptorCalls: string[] = []; - interceptors.forEach((interceptor, i) => { - addInterceptor({ - request: (config) => { - interceptorCalls.push(`Request #${i + 1}`); - - if (interceptor.request) { - return interceptor.request(config); - } - - return config; - }, - requestError: (e) => { - interceptorCalls.push(`RequestError #${i + 1}`); - if (interceptor.requestError) { - return interceptor.requestError(e); - } - - throw e; - }, - response: (res) => { - interceptorCalls.push(`Response #${i + 1}`); - - if (interceptor.response) { - return interceptor.response(res); - } - - return res; - }, - responseError: (e) => { - interceptorCalls.push(`ResponseError #${i + 1}`); - - if (interceptor.responseError) { - return interceptor.responseError(e); - } - - throw e; - }, - }); - }); - - return interceptorCalls; -} - -describe('withDefaultOptions', () => { - it('should remove undefined query params', () => { - const { query } = withDefaultOptions({ - pathname: '/withDefaultOptions', - query: { - foo: 'bar', - param1: (undefined as any) as string, - param2: (null as any) as string, - param3: '', - }, - }); - expect(query).toEqual({ foo: 'bar', param2: null, param3: '' }); - }); - - it('should add default options', () => { - expect(withDefaultOptions({ pathname: '/addDefaultOptions' })).toEqual({ - pathname: '/addDefaultOptions', - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json' }, - method: 'GET', - }); - }); -}); diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts deleted file mode 100644 index 4eb7149931575..0000000000000 --- a/src/legacy/ui/public/kfetch/kfetch.ts +++ /dev/null @@ -1,110 +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 { merge } from 'lodash'; -// @ts-ignore not really worth typing -import { KFetchError } from './kfetch_error'; - -import { HttpSetup } from '../../../../core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { HttpRequestInit } from '../../../../core/public/http/types'; - -export interface KFetchQuery { - [key: string]: string | number | boolean | undefined; -} - -export interface KFetchOptions extends HttpRequestInit { - pathname: string; - query?: KFetchQuery; - asSystemRequest?: boolean; -} - -export interface KFetchKibanaOptions { - prependBasePath?: boolean; -} - -export interface Interceptor { - request?: (config: KFetchOptions) => Promise | KFetchOptions; - requestError?: (e: any) => Promise | KFetchOptions; - response?: (res: any) => any; - responseError?: (e: any) => any; -} - -const interceptors: Interceptor[] = []; -export const resetInterceptors = () => (interceptors.length = 0); -export const addInterceptor = (interceptor: Interceptor) => interceptors.push(interceptor); - -export function createKfetch(http: HttpSetup) { - return function kfetch( - options: KFetchOptions, - { prependBasePath = true }: KFetchKibanaOptions = {} - ) { - return responseInterceptors( - requestInterceptors(withDefaultOptions(options)) - .then(({ pathname, ...restOptions }) => - http.fetch(pathname, { ...restOptions, prependBasePath }) - ) - .catch((err) => { - throw new KFetchError(err.response || { statusText: err.message }, err.body); - }) - ); - }; -} - -// Request/response interceptors are called in opposite orders. -// Request hooks start from the newest interceptor and end with the oldest. -function requestInterceptors(config: KFetchOptions): Promise { - return interceptors.reduceRight((acc, interceptor) => { - return acc.then(interceptor.request, interceptor.requestError); - }, Promise.resolve(config)); -} - -// Response hooks start from the oldest interceptor and end with the newest. -function responseInterceptors(responsePromise: Promise) { - return interceptors.reduce((acc, interceptor) => { - return acc.then(interceptor.response, interceptor.responseError); - }, responsePromise); -} - -export function withDefaultOptions(options?: KFetchOptions): KFetchOptions { - const withDefaults = merge( - { - method: 'GET', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - }, - options - ) as KFetchOptions; - - if ( - options && - options.headers && - 'Content-Type' in options.headers && - options.headers['Content-Type'] === undefined - ) { - // TS thinks headers could be undefined here, but that isn't possible because - // of the merge above. - // @ts-ignore - withDefaults.headers['Content-Type'] = undefined; - } - - return withDefaults; -} diff --git a/src/legacy/ui/public/kfetch/kfetch_error.ts b/src/legacy/ui/public/kfetch/kfetch_error.ts deleted file mode 100644 index f351959e624b8..0000000000000 --- a/src/legacy/ui/public/kfetch/kfetch_error.ts +++ /dev/null @@ -1,30 +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 KFetchError extends Error { - constructor(public readonly res: Response, public readonly body?: any) { - super(res.statusText); - - // captureStackTrace is only available in the V8 engine, so any browser using - // a different JS engine won't have access to this method. - if (Error.captureStackTrace) { - Error.captureStackTrace(this, KFetchError); - } - } -} diff --git a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js b/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js deleted file mode 100644 index efcfb77997265..0000000000000 --- a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js +++ /dev/null @@ -1,149 +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 $ from 'jquery'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import ngMock from 'ng_mock'; - -import { $setupXsrfRequestInterceptor } from '../../../../../plugins/kibana_legacy/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { version } from '../../../../../core/server/utils/package_json'; - -const xsrfHeader = 'kbn-version'; - -describe('chrome xsrf apis', function () { - const sandbox = sinon.createSandbox(); - - afterEach(function () { - sandbox.restore(); - }); - - describe('jQuery support', function () { - it('adds a global jQuery prefilter', function () { - sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(version); - expect($.ajaxPrefilter.callCount).to.be(1); - }); - - describe('jQuery prefilter', function () { - let prefilter; - - beforeEach(function () { - sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(version); - prefilter = $.ajaxPrefilter.args[0][0]; - }); - - it(`sets the ${xsrfHeader} header`, function () { - const setHeader = sinon.stub(); - prefilter({}, {}, { setRequestHeader: setHeader }); - - expect(setHeader.callCount).to.be(1); - expect(setHeader.args[0]).to.eql([xsrfHeader, version]); - }); - - it('can be canceled by setting the kbnXsrfToken option', function () { - const setHeader = sinon.stub(); - prefilter({ kbnXsrfToken: false }, {}, { setRequestHeader: setHeader }); - expect(setHeader.callCount).to.be(0); - }); - }); - - describe('Angular support', function () { - let $http; - let $httpBackend; - - beforeEach(function () { - sandbox.stub($, 'ajaxPrefilter'); - ngMock.module($setupXsrfRequestInterceptor(version)); - }); - - beforeEach( - ngMock.inject(function ($injector) { - $http = $injector.get('$http'); - $httpBackend = $injector.get('$httpBackend'); - - $httpBackend.when('POST', '/api/test').respond('ok'); - }) - ); - - afterEach(function () { - $httpBackend.verifyNoOutstandingExpectation(); - $httpBackend.verifyNoOutstandingRequest(); - }); - - it(`injects a ${xsrfHeader} header on every request`, function () { - $httpBackend - .expectPOST('/api/test', undefined, function (headers) { - return headers[xsrfHeader] === version; - }) - .respond(200, ''); - - $http.post('/api/test'); - $httpBackend.flush(); - }); - - it('skips requests with the kbnXsrfToken set falsy', function () { - $httpBackend - .expectPOST('/api/test', undefined, function (headers) { - return !(xsrfHeader in headers); - }) - .respond(200, ''); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: 0, - }); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: '', - }); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: false, - }); - - $httpBackend.flush(); - }); - - it('treats the kbnXsrfToken option as boolean-y', function () { - const customToken = `custom:${version}`; - $httpBackend - .expectPOST('/api/test', undefined, function (headers) { - return headers[xsrfHeader] === version; - }) - .respond(200, ''); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: customToken, - }); - - $httpBackend.flush(); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/legacy_compat/index.ts b/src/legacy/ui/public/legacy_compat/index.ts deleted file mode 100644 index 2067fa6489304..0000000000000 --- a/src/legacy/ui/public/legacy_compat/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 { configureAppAngularModule } from '../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/ui/public/metadata.ts b/src/legacy/ui/public/metadata.ts deleted file mode 100644 index fade0f0d8629a..0000000000000 --- a/src/legacy/ui/public/metadata.ts +++ /dev/null @@ -1,25 +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 { npSetup } from 'ui/new_platform'; - -export const metadata: { - branch: string; - version: string; -} = npSetup.core.injectedMetadata.getLegacyMetadata(); diff --git a/src/legacy/ui/public/modules.js b/src/legacy/ui/public/modules.js deleted file mode 100644 index bb1c8aead1c34..0000000000000 --- a/src/legacy/ui/public/modules.js +++ /dev/null @@ -1,142 +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 _ from 'lodash'; -/** - * This module is used by Kibana to create and reuse angular modules. Angular modules - * can only be created once and need to have their dependencies at creation. This is - * hard/impossible to do in require.js since all of the dependencies for a module are - * loaded before it is. - * - * Here is an example: - * - * In the scenario below, require.js would load directive.js first because it is a - * dependency of app.js. This would cause the call to `angular.module('app')` to - * execute before the module is actually created. This causes angular to throw an - * error. This effect is magnified when app.js links off to many different modules. - * - * This is normally solved by creating unique modules per file, listed as the 1st - * alternate solution below. Unfortunately this solution would have required that - * we replicate our require statements. - * - * app.js - * ``` - * angular.module('app', ['ui.bootstrap']) - * .controller('AppController', function () { ... }); - * - * require('./directive'); - * ``` - * - * directive.js - * ``` - * angular.module('app') - * .directive('someDirective', function () { ... }); - * ``` - * - * Before taking this approach we saw three possible solutions: - * 1. replicate our js modules in angular modules/use a different module per file - * 2. create a single module outside of our js modules and share it - * 3. use a helper lib to dynamically create modules as needed. - * - * We decided to go with #3 - * - * This ends up working by creating a list of modules that the code base creates by - * calling `modules.get(name)` with different names, and then before bootstrapping - * the application kibana uses `modules.link()` to set the dependencies of the "kibana" - * module to include every defined module. This guarantees that kibana can always find - * any angular dependency defined in the kibana code base. This **also** means that - * Private modules are able to find any dependency, since they are injected using the - * "kibana" module's injector. - * - */ -const existingModules = {}; -const links = []; - -/** - * Take an angular module and extends the dependencies for that module to include all of the modules - * created using `ui/modules` - * - * @param {AngularModule} module - the module to extend - * @return {undefined} - */ -export function link(module) { - // as modules are defined they will be set as requirements for this app - links.push(module); - - // merge in the existing modules - module.requires = _.union(module.requires, _.keys(existingModules)); -} - -/** - * The primary means of interacting with `ui/modules`. Returns an angular module. If the module already - * exists the existing version will be returned. `dependencies` are either set as or merged into the - * modules total dependencies. - * - * This is in contrast to the `angular.module(name, [dependencies])` function which will only - * create a module if the `dependencies` list is passed and get an existing module if no dependencies - * are passed. This requires knowing the order that your files will load, which we can't guarantee. - * - * @param {string} moduleName - the unique name for this module - * @param {array[string]} [requires=[]] - the other modules this module requires - * @return {AngularModule} - */ -export function get(moduleName, requires) { - let module = existingModules[moduleName]; - - if (module === void 0) { - // create the module - module = existingModules[moduleName] = angular.module(moduleName, []); - - module.close = _.partial(close, moduleName); - - // ensure that it is required by linked modules - _.each(links, function (app) { - if (!~app.requires.indexOf(moduleName)) app.requires.push(moduleName); - }); - } - - if (requires) { - // update requires list with possibly new requirements - module.requires = _.union(module.requires, requires); - } - - return module; -} - -export function close(moduleName) { - const module = existingModules[moduleName]; - - // already closed - if (!module) return; - - // if the module is currently linked, unlink it - const i = links.indexOf(module); - if (i > -1) links.splice(i, 1); - - // remove from linked modules list of required modules - _.each(links, function (app) { - _.pull(app.requires, moduleName); - }); - - // remove module from existingModules - delete existingModules[moduleName]; -} - -export const uiModules = { link, get, close }; diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts deleted file mode 100644 index 35aa8e4830428..0000000000000 --- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts +++ /dev/null @@ -1,81 +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 { coreMock } from '../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; -import { embeddablePluginMock } from '../../../../../plugins/embeddable/public/mocks'; -import { navigationPluginMock } from '../../../../../plugins/navigation/public/mocks'; -import { expressionsPluginMock } from '../../../../../plugins/expressions/public/mocks'; -import { inspectorPluginMock } from '../../../../../plugins/inspector/public/mocks'; -import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks'; -import { managementPluginMock } from '../../../../../plugins/management/public/mocks'; -import { usageCollectionPluginMock } from '../../../../../plugins/usage_collection/public/mocks'; -import { kibanaLegacyPluginMock } from '../../../../../plugins/kibana_legacy/public/mocks'; -import { chartPluginMock } from '../../../../../plugins/charts/public/mocks'; -import { advancedSettingsMock } from '../../../../../plugins/advanced_settings/public/mocks'; -import { savedObjectsManagementPluginMock } from '../../../../../plugins/saved_objects_management/public/mocks'; -import { visualizationsPluginMock } from '../../../../../plugins/visualizations/public/mocks'; -import { discoverPluginMock } from '../../../../../plugins/discover/public/mocks'; - -export const pluginsMock = { - createSetup: () => ({ - data: dataPluginMock.createSetupContract(), - charts: chartPluginMock.createSetupContract(), - navigation: navigationPluginMock.createSetupContract(), - embeddable: embeddablePluginMock.createSetupContract(), - inspector: inspectorPluginMock.createSetupContract(), - expressions: expressionsPluginMock.createSetupContract(), - uiActions: uiActionsPluginMock.createSetupContract(), - usageCollection: usageCollectionPluginMock.createSetupContract(), - advancedSettings: advancedSettingsMock.createSetupContract(), - visualizations: visualizationsPluginMock.createSetupContract(), - kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), - savedObjectsManagement: savedObjectsManagementPluginMock.createSetupContract(), - discover: discoverPluginMock.createSetupContract(), - }), - createStart: () => ({ - data: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), - navigation: navigationPluginMock.createStartContract(), - embeddable: embeddablePluginMock.createStartContract(), - inspector: inspectorPluginMock.createStartContract(), - expressions: expressionsPluginMock.createStartContract(), - uiActions: uiActionsPluginMock.createStartContract(), - management: managementPluginMock.createStartContract(), - advancedSettings: advancedSettingsMock.createStartContract(), - visualizations: visualizationsPluginMock.createStartContract(), - kibanaLegacy: kibanaLegacyPluginMock.createStartContract(), - savedObjectsManagement: savedObjectsManagementPluginMock.createStartContract(), - discover: discoverPluginMock.createStartContract(), - }), -}; - -export const createUiNewPlatformMock = () => { - const mock = { - npSetup: { - core: coreMock.createSetup(), - plugins: pluginsMock.createSetup(), - }, - npStart: { - core: coreMock.createStart(), - plugins: pluginsMock.createStart(), - }, - }; - return mock; -}; diff --git a/src/legacy/ui/public/new_platform/__mocks__/index.ts b/src/legacy/ui/public/new_platform/__mocks__/index.ts deleted file mode 100644 index d469960ddd7b7..0000000000000 --- a/src/legacy/ui/public/new_platform/__mocks__/index.ts +++ /dev/null @@ -1,23 +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 { createUiNewPlatformMock } from './helpers'; - -const { npSetup, npStart } = createUiNewPlatformMock(); -export { npSetup, npStart }; diff --git a/src/legacy/ui/public/new_platform/index.ts b/src/legacy/ui/public/new_platform/index.ts deleted file mode 100644 index ca5890854f3aa..0000000000000 --- a/src/legacy/ui/public/new_platform/index.ts +++ /dev/null @@ -1,19 +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 { __setup__, __start__, npSetup, npStart } from './new_platform'; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js deleted file mode 100644 index b8d48b784dba7..0000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ /dev/null @@ -1,550 +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 { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; -import { METRIC_TYPE } from '@kbn/analytics'; -import { setSetupServices, setStartServices } from './set_services'; -import { - AggTypesRegistry, - getAggTypes, - AggConfigs, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../src/plugins/data/common/search/aggs'; -import { ComponentRegistry } from '../../../../../src/plugins/advanced_settings/public/'; -import { UI_SETTINGS } from '../../../../../src/plugins/data/public/'; -import { - CSV_SEPARATOR_SETTING, - CSV_QUOTE_VALUES_SETTING, -} from '../../../../../src/plugins/share/public'; - -const mockObservable = () => { - return { - subscribe: () => {}, - pipe: () => { - return { - subscribe: () => {}, - }; - }, - }; -}; - -const mockComponent = () => { - return null; -}; - -let refreshInterval = undefined; -let isTimeRangeSelectorEnabled = true; -let isAutoRefreshSelectorEnabled = true; - -export const mockUiSettings = { - get: (item, defaultValue) => { - const defaultValues = { - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - 'dateFormat:tz': 'UTC', - [UI_SETTINGS.SHORT_DOTS_ENABLE]: true, - [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, - [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: true, - [UI_SETTINGS.QUERY_STRING_OPTIONS]: {}, - [UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: '($0,0.[00])', - [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[000]', - [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0,0.[000]%', - [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'en', - [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, - [CSV_SEPARATOR_SETTING]: ',', - [CSV_QUOTE_VALUES_SETTING]: true, - [UI_SETTINGS.SEARCH_QUERY_LANGUAGE]: 'kuery', - 'state:storeInSessionStorage': false, - }; - - return defaultValues[item] || defaultValue; - }, - getUpdate$: () => ({ - subscribe: sinon.fake(), - }), - isDefault: sinon.fake(), -}; - -const mockCoreSetup = { - chrome: {}, - http: { - basePath: { - get: sinon.fake.returns(''), - }, - }, - injectedMetadata: {}, - uiSettings: mockUiSettings, -}; - -const mockCoreStart = { - application: { - capabilities: {}, - }, - chrome: { - overlays: { - openModal: sinon.fake(), - }, - }, - http: { - basePath: { - get: sinon.fake.returns(''), - }, - }, - notifications: { - toasts: {}, - }, - i18n: {}, - overlays: {}, - savedObjects: { - client: {}, - }, - uiSettings: mockUiSettings, -}; - -const querySetup = { - state$: mockObservable(), - filterManager: { - getFetches$: sinon.fake(), - getFilters: sinon.fake(), - getAppFilters: sinon.fake(), - getGlobalFilters: sinon.fake(), - removeFilter: sinon.fake(), - addFilters: sinon.fake(), - setFilters: sinon.fake(), - removeAll: sinon.fake(), - getUpdates$: mockObservable, - }, - timefilter: { - timefilter: { - getFetch$: mockObservable, - getAutoRefreshFetch$: mockObservable, - getEnabledUpdated$: mockObservable, - getTimeUpdate$: mockObservable, - getRefreshIntervalUpdate$: mockObservable, - isTimeRangeSelectorEnabled: () => { - return isTimeRangeSelectorEnabled; - }, - isAutoRefreshSelectorEnabled: () => { - return isAutoRefreshSelectorEnabled; - }, - disableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = false; - }, - enableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = true; - }, - getRefreshInterval: () => { - return refreshInterval; - }, - setRefreshInterval: (interval) => { - refreshInterval = interval; - }, - enableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = true; - }, - disableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = false; - }, - getTime: sinon.fake(), - setTime: sinon.fake(), - getActiveBounds: sinon.fake(), - getBounds: sinon.fake(), - calculateBounds: sinon.fake(), - createFilter: sinon.fake(), - }, - history: sinon.fake(), - }, - savedQueries: { - saveQuery: sinon.fake(), - getAllSavedQueries: sinon.fake(), - findSavedQueries: sinon.fake(), - getSavedQuery: sinon.fake(), - deleteSavedQuery: sinon.fake(), - getSavedQueryCount: sinon.fake(), - }, -}; - -const mockAggTypesRegistry = () => { - const registry = new AggTypesRegistry(); - const registrySetup = registry.setup(); - const aggTypes = getAggTypes({ - calculateBounds: sinon.fake(), - getConfig: sinon.fake(), - getFieldFormatsStart: () => ({ - deserialize: sinon.fake(), - getDefaultInstance: sinon.fake(), - }), - isDefaultTimezone: () => true, - }); - aggTypes.buckets.forEach((type) => registrySetup.registerBucket(type)); - aggTypes.metrics.forEach((type) => registrySetup.registerMetric(type)); - - return registry; -}; - -const aggTypesRegistry = mockAggTypesRegistry(); - -export const npSetup = { - core: mockCoreSetup, - plugins: { - advancedSettings: { - component: { - register: sinon.fake(), - componentType: ComponentRegistry.componentType, - }, - }, - usageCollection: { - allowTrackUserAgent: sinon.fake(), - reportUiStats: sinon.fake(), - METRIC_TYPE, - }, - embeddable: { - registerEmbeddableFactory: sinon.fake(), - }, - expressions: { - registerFunction: sinon.fake(), - registerRenderer: sinon.fake(), - registerType: sinon.fake(), - }, - data: { - autocomplete: { - addProvider: sinon.fake(), - getProvider: sinon.fake(), - }, - query: querySetup, - search: { - aggs: { - types: aggTypesRegistry.setup(), - }, - __LEGACY: { - esClient: { - search: sinon.fake(), - msearch: sinon.fake(), - }, - }, - }, - fieldFormats: getFieldFormatsRegistry(mockCoreSetup), - }, - share: { - register: () => {}, - urlGenerators: { - registerUrlGenerator: () => {}, - }, - }, - devTools: { - register: () => {}, - }, - kibanaLegacy: { - registerLegacyApp: () => {}, - forwardApp: () => {}, - config: { - defaultAppId: 'home', - }, - }, - inspector: { - registerView: () => undefined, - __LEGACY: { - views: { - register: () => undefined, - }, - }, - }, - uiActions: { - attachAction: sinon.fake(), - registerAction: sinon.fake(), - registerTrigger: sinon.fake(), - }, - home: { - featureCatalogue: { - register: sinon.fake(), - }, - environment: { - update: sinon.fake(), - }, - config: { - disableWelcomeScreen: false, - }, - tutorials: { - setVariable: sinon.fake(), - }, - }, - charts: { - theme: { - chartsTheme$: mockObservable, - useChartsTheme: sinon.fake(), - }, - colors: { - seedColors: ['white', 'black'], - }, - }, - management: { - sections: { - getSection: () => ({ - registerApp: sinon.fake(), - }), - }, - }, - indexPatternManagement: { - list: { addListConfig: sinon.fake() }, - creation: { addCreationConfig: sinon.fake() }, - }, - discover: { - docViews: { - addDocView: sinon.fake(), - setAngularInjectorGetter: sinon.fake(), - }, - }, - visTypeVega: { - config: sinon.fake(), - }, - visualizations: { - createBaseVisualization: sinon.fake(), - createReactVisualization: sinon.fake(), - registerAlias: sinon.fake(), - hideTypes: sinon.fake(), - }, - - mapsLegacy: { - serviceSettings: sinon.fake(), - getPrecision: sinon.fake(), - getZoomPrecision: sinon.fake(), - }, - }, -}; - -export const npStart = { - core: mockCoreStart, - plugins: { - management: { - legacy: { - getSection: () => ({ - register: sinon.fake(), - deregister: sinon.fake(), - hasItem: sinon.fake(), - }), - }, - sections: { - getSection: () => ({ - registerApp: sinon.fake(), - }), - }, - }, - indexPatternManagement: { - list: { - getType: sinon.fake(), - getIndexPatternCreationOptions: sinon.fake(), - }, - creation: { - getIndexPatternTags: sinon.fake(), - getFieldInfo: sinon.fake(), - areScriptedFieldsEnabled: sinon.fake(), - }, - }, - embeddable: { - getEmbeddableFactory: sinon.fake(), - getEmbeddableFactories: sinon.fake(), - registerEmbeddableFactory: sinon.fake(), - }, - expressions: { - registerFunction: sinon.fake(), - registerRenderer: sinon.fake(), - registerType: sinon.fake(), - }, - kibanaLegacy: { - getForwards: () => [], - loadFontAwesome: () => {}, - config: { - defaultAppId: 'home', - }, - dashboardConfig: { - turnHideWriteControlsOn: sinon.fake(), - getHideWriteControls: sinon.fake(), - }, - }, - dashboard: { - getSavedDashboardLoader: sinon.fake(), - }, - data: { - actions: { - createFiltersFromValueClickAction: Promise.resolve(['yes']), - createFiltersFromRangeSelectAction: sinon.fake(), - }, - autocomplete: { - getProvider: sinon.fake(), - }, - getSuggestions: sinon.fake(), - indexPatterns: { - get: sinon.spy((indexPatternId) => - Promise.resolve({ - id: indexPatternId, - isTimeNanosBased: () => false, - popularizeField: () => {}, - }) - ), - }, - ui: { - IndexPatternSelect: mockComponent, - SearchBar: mockComponent, - }, - query: { - filterManager: { - getFetches$: sinon.fake(), - getFilters: sinon.fake(), - getAppFilters: sinon.fake(), - getGlobalFilters: sinon.fake(), - removeFilter: sinon.fake(), - addFilters: sinon.fake(), - setFilters: sinon.fake(), - removeAll: sinon.fake(), - getUpdates$: mockObservable, - }, - timefilter: { - timefilter: { - getFetch$: mockObservable, - getAutoRefreshFetch$: mockObservable, - getEnabledUpdated$: mockObservable, - getTimeUpdate$: mockObservable, - getRefreshIntervalUpdate$: mockObservable, - isTimeRangeSelectorEnabled: () => { - return isTimeRangeSelectorEnabled; - }, - isAutoRefreshSelectorEnabled: () => { - return isAutoRefreshSelectorEnabled; - }, - disableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = false; - }, - enableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = true; - }, - getRefreshInterval: () => { - return refreshInterval; - }, - setRefreshInterval: (interval) => { - refreshInterval = interval; - }, - enableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = true; - }, - disableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = false; - }, - getTime: sinon.fake(), - setTime: sinon.fake(), - getActiveBounds: sinon.fake(), - getBounds: sinon.fake(), - calculateBounds: sinon.fake(), - createFilter: sinon.fake(), - }, - history: sinon.fake(), - }, - }, - search: { - aggs: { - calculateAutoTimeExpression: sinon.fake(), - createAggConfigs: (indexPattern, configStates = []) => { - return new AggConfigs(indexPattern, configStates, { - typesRegistry: aggTypesRegistry.start(), - fieldFormats: getFieldFormatsRegistry(mockCoreStart), - }); - }, - types: aggTypesRegistry.start(), - }, - __LEGACY: { - esClient: { - search: sinon.fake(), - msearch: sinon.fake(), - }, - }, - }, - fieldFormats: getFieldFormatsRegistry(mockCoreStart), - }, - share: { - toggleShareContextMenu: () => {}, - }, - inspector: { - isAvailable: () => false, - open: () => ({ - onClose: Promise.resolve(undefined), - close: () => Promise.resolve(undefined), - }), - }, - uiActions: { - attachAction: sinon.fake(), - registerAction: sinon.fake(), - registerTrigger: sinon.fake(), - detachAction: sinon.fake(), - executeTriggerActions: sinon.fake(), - getTrigger: sinon.fake(), - getTriggerActions: sinon.fake(), - getTriggerCompatibleActions: sinon.fake(), - }, - visualizations: { - get: sinon.fake(), - all: sinon.fake(), - getAliases: sinon.fake(), - savedVisualizationsLoader: {}, - showNewVisModal: sinon.fake(), - createVis: sinon.fake(), - convertFromSerializedVis: sinon.fake(), - convertToSerializedVis: sinon.fake(), - }, - navigation: { - ui: { - TopNavMenu: mockComponent, - }, - }, - charts: { - theme: { - chartsTheme$: mockObservable, - useChartsTheme: sinon.fake(), - }, - }, - discover: { - docViews: { - DocViewer: () => null, - }, - savedSearchLoader: {}, - }, - }, -}; - -export function __setup__(coreSetup) { - npSetup.core = coreSetup; - - // no-op application register calls (this is overwritten to - // bootstrap an LP plugin outside of tests) - npSetup.core.application.register = () => {}; - - npSetup.core.uiSettings.get = mockUiSettings.get; - - // Services that need to be set in the legacy platform since the legacy data - // & vis plugins which previously provided them have been removed. - setSetupServices(npSetup); -} - -export function __start__(coreStart) { - npStart.core = coreStart; - - npStart.core.uiSettings.get = mockUiSettings.get; - - // Services that need to be set in the legacy platform since the legacy data - // & vis plugins which previously provided them have been removed. - setStartServices(npStart); -} diff --git a/src/legacy/ui/public/new_platform/new_platform.test.mocks.ts b/src/legacy/ui/public/new_platform/new_platform.test.mocks.ts deleted file mode 100644 index f44efe17ef8ee..0000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.test.mocks.ts +++ /dev/null @@ -1,31 +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 { scopedHistoryMock } from '../../../../core/public/mocks'; - -export const setRootControllerMock = jest.fn(); - -jest.doMock('ui/chrome', () => ({ - setRootController: setRootControllerMock, -})); - -export const historyMock = scopedHistoryMock.create(); -jest.doMock('../../../../core/public', () => ({ - ScopedHistory: jest.fn(() => historyMock), -})); diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts deleted file mode 100644 index d515c348ca440..0000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ /dev/null @@ -1,124 +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. - */ - -jest.mock('history'); - -import { setRootControllerMock, historyMock } from './new_platform.test.mocks'; -import { legacyAppRegister, __reset__, __setup__, __start__ } from './new_platform'; -import { coreMock } from '../../../../core/public/mocks'; -import { AppMount } from '../../../../core/public'; - -describe('ui/new_platform', () => { - describe('legacyAppRegister', () => { - beforeEach(() => { - setRootControllerMock.mockReset(); - __reset__(); - __setup__(coreMock.createSetup({ basePath: '/test/base/path' }) as any, {} as any); - }); - - const registerApp = () => { - const unmountMock = jest.fn(); - const mountMock = jest.fn, Parameters>(() => unmountMock); - legacyAppRegister({ - id: 'test', - title: 'Test', - mount: mountMock, - }); - return { mountMock, unmountMock }; - }; - - test('sets ui/chrome root controller', () => { - registerApp(); - expect(setRootControllerMock).toHaveBeenCalledWith('test', expect.any(Function)); - }); - - test('throws if called more than once', () => { - registerApp(); - expect(registerApp).toThrowErrorMatchingInlineSnapshot( - `"core.application.register may only be called once for legacy plugins."` - ); - }); - - test('controller calls app.mount when invoked', () => { - const { mountMock } = registerApp(); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - expect(mountMock).toHaveBeenCalledWith({ - element: expect.any(HTMLElement), - appBasePath: '/test/base/path/app/test', - onAppLeave: expect.any(Function), - history: historyMock, - }); - }); - - test('app is mounted in new div inside containing element', () => { - const { mountMock } = registerApp(); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - - const { element } = mountMock.mock.calls[0][0]; - expect(element.parentElement).toEqual(elementMock[0]); - }); - - test('controller calls deprecated context app.mount when invoked', () => { - const unmountMock = jest.fn(); - // Two arguments changes how this is called. - const mountMock = jest.fn((context, params) => unmountMock); - legacyAppRegister({ - id: 'test', - title: 'Test', - mount: mountMock, - }); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { - element: expect.any(HTMLElement), - appBasePath: '/test/base/path/app/test', - onAppLeave: expect.any(Function), - history: historyMock, - }); - }); - - test('controller calls unmount when $scope.$destroy', async () => { - const { unmountMock } = registerApp(); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - // Flush promise queue. Must be done this way because the controller cannot return a Promise without breaking - // angular. - await new Promise((resolve) => setTimeout(resolve, 1)); - - const [event, eventHandler] = scopeMock.$on.mock.calls[0]; - expect(event).toEqual('$destroy'); - eventHandler(); - expect(unmountMock).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts deleted file mode 100644 index 37787ffbde4fc..0000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ /dev/null @@ -1,208 +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 { IScope } from 'angular'; - -import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; -import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; -import { createBrowserHistory } from 'history'; -import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; -import { DashboardStart } from '../../../../plugins/dashboard/public'; -import { setSetupServices, setStartServices } from './set_services'; -import { - LegacyCoreSetup, - LegacyCoreStart, - App, - AppMountDeprecated, - ScopedHistory, -} from '../../../../core/public'; -import { Plugin as DataPlugin } from '../../../../plugins/data/public'; -import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; -import { - Setup as InspectorSetup, - Start as InspectorStart, -} from '../../../../plugins/inspector/public'; -import { ChartsPluginSetup, ChartsPluginStart } from '../../../../plugins/charts/public'; -import { DevToolsSetup } from '../../../../plugins/dev_tools/public'; -import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public'; -import { HomePublicPluginSetup } from '../../../../plugins/home/public'; -import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public'; -import { - AdvancedSettingsSetup, - AdvancedSettingsStart, -} from '../../../../plugins/advanced_settings/public'; -import { ManagementSetup, ManagementStart } from '../../../../plugins/management/public'; -import { - IndexPatternManagementSetup, - IndexPatternManagementStart, -} from '../../../../plugins/index_pattern_management/public'; -import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public'; -import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; -import { TelemetryPluginSetup, TelemetryPluginStart } from '../../../../plugins/telemetry/public'; -import { - NavigationPublicPluginSetup, - NavigationPublicPluginStart, -} from '../../../../plugins/navigation/public'; -import { DiscoverSetup, DiscoverStart } from '../../../../plugins/discover/public'; -import { - SavedObjectsManagementPluginSetup, - SavedObjectsManagementPluginStart, -} from '../../../../plugins/saved_objects_management/public'; -import { - VisualizationsSetup, - VisualizationsStart, -} from '../../../../plugins/visualizations/public'; -import { VisTypeTimelionPluginStart } from '../../../../plugins/vis_type_timelion/public'; -import { MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; - -export interface PluginsSetup { - bfetch: BfetchPublicSetup; - charts: ChartsPluginSetup; - data: ReturnType; - embeddable: EmbeddableSetup; - expressions: ReturnType; - home: HomePublicPluginSetup; - inspector: InspectorSetup; - uiActions: UiActionsSetup; - navigation: NavigationPublicPluginSetup; - devTools: DevToolsSetup; - kibanaLegacy: KibanaLegacySetup; - share: SharePluginSetup; - usageCollection: UsageCollectionSetup; - advancedSettings: AdvancedSettingsSetup; - management: ManagementSetup; - discover: DiscoverSetup; - visualizations: VisualizationsSetup; - telemetry?: TelemetryPluginSetup; - savedObjectsManagement: SavedObjectsManagementPluginSetup; - mapsLegacy: MapsLegacyPluginSetup; - indexPatternManagement: IndexPatternManagementSetup; - visTypeXy?: VisTypeXyPluginSetup; -} - -export interface PluginsStart { - bfetch: BfetchPublicStart; - charts: ChartsPluginStart; - data: ReturnType; - embeddable: EmbeddableStart; - expressions: ReturnType; - inspector: InspectorStart; - uiActions: UiActionsStart; - navigation: NavigationPublicPluginStart; - kibanaLegacy: KibanaLegacyStart; - share: SharePluginStart; - management: ManagementStart; - advancedSettings: AdvancedSettingsStart; - discover: DiscoverStart; - visualizations: VisualizationsStart; - telemetry?: TelemetryPluginStart; - dashboard: DashboardStart; - savedObjectsManagement: SavedObjectsManagementPluginStart; - visTypeTimelion: VisTypeTimelionPluginStart; - indexPatternManagement: IndexPatternManagementStart; -} - -export const npSetup = { - core: (null as unknown) as LegacyCoreSetup, - plugins: {} as PluginsSetup, -}; - -export const npStart = { - core: (null as unknown) as LegacyCoreStart, - plugins: {} as PluginsStart, -}; - -/** - * Only used by unit tests - * @internal - */ -export function __reset__() { - npSetup.core = (null as unknown) as LegacyCoreSetup; - npSetup.plugins = {} as any; - npStart.core = (null as unknown) as LegacyCoreStart; - npStart.plugins = {} as any; - legacyAppRegistered = false; -} - -export function __setup__(coreSetup: LegacyCoreSetup, plugins: PluginsSetup) { - npSetup.core = coreSetup; - npSetup.plugins = plugins; - - // Setup compatibility layer for AppService in legacy platform - npSetup.core.application.register = legacyAppRegister; - - // Services that need to be set in the legacy platform since the legacy data - // & vis plugins which previously provided them have been removed. - setSetupServices(npSetup); -} - -export function __start__(coreStart: LegacyCoreStart, plugins: PluginsStart) { - npStart.core = coreStart; - npStart.plugins = plugins; - - // Services that need to be set in the legacy platform since the legacy data - // & vis plugins which previously provided them have been removed. - setStartServices(npStart); -} - -/** Flag used to ensure `legacyAppRegister` is only called once. */ -let legacyAppRegistered = false; - -/** - * Exported for testing only. Use `npSetup.core.application.register` in legacy apps. - * @internal - */ -export const legacyAppRegister = (app: App) => { - if (legacyAppRegistered) { - throw new Error(`core.application.register may only be called once for legacy plugins.`); - } - legacyAppRegistered = true; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - require('ui/chrome').setRootController(app.id, ($scope: IScope, $element: JQLite) => { - const element = document.createElement('div'); - $element[0].appendChild(element); - - // Root controller cannot return a Promise so use an internal async function and call it immediately - (async () => { - const appRoute = app.appRoute || `/app/${app.id}`; - const appBasePath = npSetup.core.http.basePath.prepend(appRoute); - const params = { - element, - appBasePath, - history: new ScopedHistory( - createBrowserHistory({ basename: npSetup.core.http.basePath.get() }), - appRoute - ), - onAppLeave: () => undefined, - }; - const unmount = isAppMountDeprecated(app.mount) - ? await app.mount({ core: npStart.core }, params) - : await app.mount(params); - $scope.$on('$destroy', () => { - unmount(); - }); - })(); - }); -}; - -function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { - // Mount functions with two arguments are assumed to expect deprecated `context` object. - return mount.length === 2; -} diff --git a/src/legacy/ui/public/new_platform/set_services.test.ts b/src/legacy/ui/public/new_platform/set_services.test.ts deleted file mode 100644 index 74e789ec220b1..0000000000000 --- a/src/legacy/ui/public/new_platform/set_services.test.ts +++ /dev/null @@ -1,51 +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 { __reset__, __setup__, __start__, PluginsSetup, PluginsStart } from './new_platform'; -import * as dataServices from '../../../../plugins/data/public/services'; -import * as visualizationsServices from '../../../../plugins/visualizations/public/services'; -import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; -import { coreMock } from '../../../../core/public/mocks'; -import { npSetup, npStart } from './__mocks__'; - -describe('ui/new_platform', () => { - describe('set service getters', () => { - const testServiceGetters = (name: string, services: Record) => { - const getters = Object.keys(services).filter((k) => k.substring(0, 3) === 'get'); - getters.forEach((g) => { - it(`ui/new_platform sets a value for ${name} getter ${g}`, () => { - __reset__(); - __setup__( - (coreMock.createSetup() as unknown) as LegacyCoreSetup, - (npSetup.plugins as unknown) as PluginsSetup - ); - __start__( - (coreMock.createStart() as unknown) as LegacyCoreStart, - (npStart.plugins as unknown) as PluginsStart - ); - - expect(services[g]()).toBeDefined(); - }); - }); - }; - - testServiceGetters('data', dataServices); - testServiceGetters('visualizations', visualizationsServices); - }); -}); diff --git a/src/legacy/ui/public/new_platform/set_services.ts b/src/legacy/ui/public/new_platform/set_services.ts deleted file mode 100644 index 036157a9f3fbc..0000000000000 --- a/src/legacy/ui/public/new_platform/set_services.ts +++ /dev/null @@ -1,87 +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 { pick } from 'lodash'; - -import { PluginsSetup, PluginsStart } from './new_platform'; -import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import * as dataServices from '../../../../plugins/data/public/services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import * as visualizationsServices from '../../../../plugins/visualizations/public/services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createSavedVisLoader } from '../../../../plugins/visualizations/public/saved_visualizations/saved_visualizations'; - -interface NpSetup { - core: LegacyCoreSetup; - plugins: PluginsSetup; -} - -interface NpStart { - core: LegacyCoreStart; - plugins: PluginsStart; -} - -export function setSetupServices(npSetup: NpSetup) { - // Services that need to be set in the legacy platform since the legacy data plugin - // which previously provided them has been removed. - visualizationsServices.setUISettings(npSetup.core.uiSettings); - visualizationsServices.setUsageCollector(npSetup.plugins.usageCollection); -} - -export function setStartServices(npStart: NpStart) { - // Services that need to be set in the legacy platform since the legacy data plugin - // which previously provided them has been removed. - dataServices.setNotifications(npStart.core.notifications); - dataServices.setOverlays(npStart.core.overlays); - dataServices.setUiSettings(npStart.core.uiSettings); - dataServices.setFieldFormats(npStart.plugins.data.fieldFormats); - dataServices.setIndexPatterns(npStart.plugins.data.indexPatterns); - dataServices.setQueryService(npStart.plugins.data.query); - dataServices.setSearchService(npStart.plugins.data.search); - - visualizationsServices.setI18n(npStart.core.i18n); - visualizationsServices.setTypes( - pick(npStart.plugins.visualizations, ['get', 'all', 'getAliases']) - ); - visualizationsServices.setCapabilities(npStart.core.application.capabilities); - visualizationsServices.setHttp(npStart.core.http); - visualizationsServices.setApplication(npStart.core.application); - visualizationsServices.setEmbeddable(npStart.plugins.embeddable); - visualizationsServices.setSavedObjects(npStart.core.savedObjects); - visualizationsServices.setIndexPatterns(npStart.plugins.data.indexPatterns); - visualizationsServices.setFilterManager(npStart.plugins.data.query.filterManager); - visualizationsServices.setExpressions(npStart.plugins.expressions); - visualizationsServices.setUiActions(npStart.plugins.uiActions); - visualizationsServices.setTimeFilter(npStart.plugins.data.query.timefilter.timefilter); - visualizationsServices.setAggs(npStart.plugins.data.search.aggs); - visualizationsServices.setOverlays(npStart.core.overlays); - visualizationsServices.setChrome(npStart.core.chrome); - visualizationsServices.setSearch(npStart.plugins.data.search); - const savedVisualizationsLoader = createSavedVisLoader({ - savedObjectsClient: npStart.core.savedObjects.client, - indexPatterns: npStart.plugins.data.indexPatterns, - search: npStart.plugins.data.search, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, - visualizationTypes: visualizationsServices.getTypes(), - }); - visualizationsServices.setSavedVisualizationsLoader(savedVisualizationsLoader); - visualizationsServices.setSavedSearchLoader(npStart.plugins.discover.savedSearchLoader); -} diff --git a/src/legacy/ui/public/notify/banners/BANNERS.md b/src/legacy/ui/public/notify/banners/BANNERS.md deleted file mode 100644 index fc6bddc3aa4ed..0000000000000 --- a/src/legacy/ui/public/notify/banners/BANNERS.md +++ /dev/null @@ -1,360 +0,0 @@ -# Banners - -Use this service to surface banners at the top of the screen. The expectation is that the banner will used an -`` to render, but that is not a requirement. See [the EUI docs](https://elastic.github.io/eui/) for -more information on banners and their role within the UI. - -Banners should be considered with respect to their lifecycle. Most banners are best served by using the `add` and -`remove` functions. - -## Importing the module - -```js -import { banners } from 'ui/notify'; -``` - -## Interface - -There are three methods defined to manipulate the list of banners: `add`, `set`, and `remove`. A fourth method, -`onChange` exists to listen to changes made via `add`, `set`, and `remove`. - -### `add()` - -This is the preferred way to add banners because it implies the best usage of the banner: added once during a page's -lifecycle. For other usages, consider *not* using a banner. - -#### Syntax - -```js -const bannerId = banners.add({ - // required: - component, - // optional: - priority, -}); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `priority` | Number | Optional priority, which defaults to `0` used to place the banner. | - -To add a banner, you only need to define the `component` field. - -The `priority` sorts in descending order. Items sharing the same priority are sorted from oldest to newest. For example: - -```js -const banner1 = banners.add({ component: }); -const banner2 = banners.add({ component: , priority: 0 }); -const banner3 = banners.add({ component: , priority: 1 }); -``` - -That would be displayed as: - -``` -[ fake3 ] -[ fake1 ] -[ fake2 ] -``` - -##### Returns - -| Type | Description | -|------|-------------| -| String | A newly generated ID. | - -#### Example - -This example includes buttons that allow the user to remove the banner. In some cases, you may not want any buttons -and in other cases you will want an action to proceed the banner's removal (e.g., apply an Advanced Setting). - -This makes the most sense to use when a banner is added at the beginning of the page life cycle and not expected to -be touched, except by its own buttons triggering an action or navigating away. - -```js -const bannerId = banners.add({ - component: ( - - - - banners.remove(bannerId)} - > - Dismiss - - - - window.alert('Do Something Else')} - > - Do Something Else - - - - - ), -}); -``` - -### `remove()` - -Unlike toast notifications, banners stick around until they are explicitly removed. Using the `add` example above,you can remove it by calling `remove`. - -Note: They will stick around as long as the scope is remembered by whatever set it; navigating away won't remove it -unless the scope is forgotten (e.g., when the "app" changes)! - -#### Syntax - -```js -const removed = banners.remove(bannerId); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `id` | String | ID of a banner. | - -##### Returns - -| Type | Description | -|------|-------------| -| Boolean | `true` if the ID was recognized and the banner was removed. `false` otherwise. | - -#### Example - -To remove a banner, you need to pass the `id` of the banner. - -```js -if (banners.remove(bannerId)) { - // removed; otherwise it didn't exist (maybe it was already removed) -} -``` - -#### Scheduled removal - -Like toast notifications do automatically, you can have a banner automatically removed after a set of time, by -setting a timer: - -```js -setTimeout(() => banners.remove(bannerId), 15000); -``` - -Note: It is safe to remove a banner more than once as unknown IDs will be ignored. - -### `set()` - -Banners can be replaced once added by supplying their `id`. If one is supplied, then the ID will be used to replace -any banner with the same ID and a **new** `id` will be returned. - -You should only consider using `set` when the banner is manipulated frequently in the lifecycle of the page, where -maintaining the banner's `id` can be a burden. It is easier to allow `banners` to create the ID for you in most -situations where a banner is useful (e.g., set once), which safely avoids any chance to have an ID-based collision, -which happens automatically with `add`. - -Usage of `set` can imply that your use case is abusing the banner system. - -Note: `set` will only trigger the callback once for both the implicit remove and add operation. - -#### Syntax - -```js -const id = banners.set({ - // required: - component, - // optional: - id, - priority, -}); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `id` | String | Optional ID used to remove an existing banner. | -| `priority` | Number | Optional priority, which defaults to `0` used to place the banner. | - -The `id` is optional because it follows the same semantics as the `remove` method: unknown IDs are ignored. This -is useful when first creating a banner so that you do not have to call `add` instead. - -##### Returns - -| Type | Description | -|------|-------------| -| String | A newly generated ID. | - -#### Example - -This example does not include any way for the user to clear the banner directly. Instead, it is cleared based on -time. Related to it being cleared by time, it can also reappear within the same page life cycle by navigating between -different paths that need it displayed. Instead of adding a new banner for every navigation, you should replace any -existing banner. - -```js -let bannerId; -let timeoutId; - -function displayBanner() { - clearTimeout(timeoutId); - - bannerId = banners.set({ - id: bannerId, // the first time it will be undefined, but reused as long as this is in the same lifecycle - component: ( - - ) - }); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - banner.timeoutId = setTimeout(() => { - banners.remove(bannerId); - timeoutId = undefined; - }, 6000); -} -``` - -### `onChange()` - -For React components that intend to display the banners, it is not enough to simply `render` the `banners.list` -values. Because they can change after being rendered, the React component that renders the list must be alerted -to changes to the list. - -#### Syntax - -```js -// inside your React component -banners.onChange(() => this.forceUpdate()); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `callback` | Function | The function to invoke whenever the internal banner list is changed. | - -Every new `callback` replaces the previous callback. So calling this with `null` or `undefined` will unset the -callback. - -##### Returns - -Nothing. - -#### Example - -This can be used inside of a React component to trigger a re-`render` of the banners. - -```js -import { GlobalBannerList } from 'ui/notify'; - - -``` - -### `list` - -For React components that intend to display the banners, it is not enough to simply `render` the `banners.list` -values. Because they can change after being rendered, the React component that renders the list must be alerted -to changes to the list. - -#### Syntax - -```js - -``` - -##### Returns - -| Type | Description | -|------|-------------| -| Array | The array of banner objects. | - -Banner objects are sorted in descending order based on their `priority`, in the form: - -```js -{ - id: 'banner-123', - component: , - priority: 12, -} -``` - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `id` | String | The ID of the banner, which can be used as a React "key". | -| `priority` | Number | The priority of the banner. | - -#### Example - -This can be used to supply the banners to the `GlobalBannerList` React component (which is done for you). - -```js -import { GlobalBannerList } from 'ui/notify'; - - -``` - -## Use in functional tests - -Functional tests are commonly used to verify that an action yielded a successful outcome. You can place a -`data-test-subj` attribute on the banner and use it to check if the banner exists inside of your functional test. -This acts as a proxy for verifying the successful outcome. Any unrecognized field will be added as a property of the -containing element. - -```js -banners.add({ - component: ( - - ), - data-test-subj: 'my-tested-banner', -}); -``` - -This will apply the `data-test-subj` to the element containing the `component`, so the inner HTML of that element -will exclusively be the specified `component`. - -Given that `component` is expected to be a React component, you could also add the `data-test-subj` directly to it: - -```js -banners.add({ - component: ( - - ), -}); -``` \ No newline at end of file diff --git a/src/legacy/ui/public/notify/banners/banners.tsx b/src/legacy/ui/public/notify/banners/banners.tsx deleted file mode 100644 index 777e408b8dfc4..0000000000000 --- a/src/legacy/ui/public/notify/banners/banners.tsx +++ /dev/null @@ -1,85 +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 React from 'react'; -import ReactDOM from 'react-dom'; -import { npStart } from 'ui/new_platform'; -import { I18nProvider } from '@kbn/i18n/react'; - -const npBanners = npStart.core.overlays.banners; - -/** compatibility layer for new platform */ -const mountForComponent = (component: React.ReactElement) => (element: HTMLElement) => { - ReactDOM.render({component}, element); - return () => ReactDOM.unmountComponentAtNode(element); -}; - -/** - * Banners represents a prioritized list of displayed components. - */ -export class Banners { - /** - * Add a new banner. - * - * @param {Object} component The React component to display. - * @param {Number} priority The optional priority order to display this banner. Higher priority values are shown first. - * @return {String} A newly generated ID. This value can be used to remove/replace the banner. - */ - add = ({ component, priority }: { component: React.ReactElement; priority?: number }) => { - return npBanners.add(mountForComponent(component), priority); - }; - - /** - * Remove an existing banner. - * - * @param {String} id The ID of the banner to remove. - * @return {Boolean} {@code true} if the ID is recognized and the banner is removed. {@code false} otherwise. - */ - remove = (id: string): boolean => { - return npBanners.remove(id); - }; - - /** - * Replace an existing banner by removing it, if it exists, and adding a new one in its place. - * - * This is similar to calling banners.remove, followed by banners.add, except that it only notifies the listener - * after adding. - * - * @param {Object} component The React component to display. - * @param {String} id The ID of the Banner to remove. - * @param {Number} priority The optional priority order to display this banner. Higher priority values are shown first. - * @return {String} A newly generated ID. This value can be used to remove/replace the banner. - */ - set = ({ - component, - id, - priority = 0, - }: { - component: React.ReactElement; - id: string; - priority?: number; - }): string => { - return npBanners.replace(id, mountForComponent(component), priority); - }; -} - -/** - * A singleton instance meant to represent all Kibana banners. - */ -export const banners = new Banners(); diff --git a/src/legacy/ui/public/notify/banners/index.ts b/src/legacy/ui/public/notify/banners/index.ts deleted file mode 100644 index 9221f95074cd9..0000000000000 --- a/src/legacy/ui/public/notify/banners/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 { banners } from './banners'; diff --git a/src/legacy/ui/public/notify/fatal_error.ts b/src/legacy/ui/public/notify/fatal_error.ts deleted file mode 100644 index 5614ffea7913e..0000000000000 --- a/src/legacy/ui/public/notify/fatal_error.ts +++ /dev/null @@ -1,25 +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 { npSetup } from 'ui/new_platform'; -import { AngularHttpError, addFatalError } from '../../../../plugins/kibana_legacy/public'; - -export function fatalError(error: AngularHttpError | Error | string, location?: string) { - addFatalError(npSetup.core.fatalErrors, error, location); -} diff --git a/src/legacy/ui/public/notify/index.d.ts b/src/legacy/ui/public/notify/index.d.ts deleted file mode 100644 index 3c1a7ba02db72..0000000000000 --- a/src/legacy/ui/public/notify/index.d.ts +++ /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 { toastNotifications } from './toasts'; -export { fatalError } from './fatal_error'; diff --git a/src/legacy/ui/public/notify/index.js b/src/legacy/ui/public/notify/index.js deleted file mode 100644 index 51394033e4d2e..0000000000000 --- a/src/legacy/ui/public/notify/index.js +++ /dev/null @@ -1,22 +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 { fatalError } from './fatal_error'; -export { toastNotifications } from './toasts'; -export { banners } from './banners'; diff --git a/src/legacy/ui/public/notify/toasts/index.ts b/src/legacy/ui/public/notify/toasts/index.ts deleted file mode 100644 index c5e8e3b7ca7a3..0000000000000 --- a/src/legacy/ui/public/notify/toasts/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 { ToastNotifications } from './toast_notifications'; -export { toastNotifications } from './toasts'; diff --git a/src/legacy/ui/public/notify/toasts/toast_notifications.ts b/src/legacy/ui/public/notify/toasts/toast_notifications.ts deleted file mode 100644 index d3ec8edb5d73a..0000000000000 --- a/src/legacy/ui/public/notify/toasts/toast_notifications.ts +++ /dev/null @@ -1,22 +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. - */ -/** - * ToastNotifications is deprecated! Please use npSetup.core.notifications.toasts instead - */ -export { ToastNotifications } from '../../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/ui/public/notify/toasts/toasts.ts b/src/legacy/ui/public/notify/toasts/toasts.ts deleted file mode 100644 index 15be7a7891553..0000000000000 --- a/src/legacy/ui/public/notify/toasts/toasts.ts +++ /dev/null @@ -1,22 +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 { npSetup } from 'ui/new_platform'; -import { ToastNotifications } from './toast_notifications'; -export const toastNotifications = new ToastNotifications(npSetup.core.notifications.toasts); diff --git a/src/legacy/ui/public/private/__tests__/private.js b/src/legacy/ui/public/private/__tests__/private.js deleted file mode 100644 index 1f9d696bb440f..0000000000000 --- a/src/legacy/ui/public/private/__tests__/private.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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -describe('Private module loader', function () { - let Private; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function ($injector) { - Private = $injector.get('Private'); - }) - ); - - it('accepts a provider that will be called to init a module', function () { - const football = {}; - function Provider() { - return football; - } - - const instance = Private(Provider); - expect(instance).to.be(football); - }); - - it('injects angular dependencies into the Provider', function () { - function Provider(Private) { - return Private; - } - - const instance = Private(Provider); - expect(instance).to.be(Private); - }); - - it('detects circular dependencies', function () { - expect(function () { - function Provider1() { - Private(Provider2); - } - - function Provider2() { - Private(Provider1); - } - - Private(Provider1); - }).to.throwException(/circular/i); - }); - - it('always provides the same instance form the Provider', function () { - function Provider() { - return {}; - } - - expect(Private(Provider)).to.be(Private(Provider)); - }); - - describe('#stub', function () { - it('accepts a replacement instance for a Provider', function () { - const replaced = {}; - const replacement = {}; - - function Provider() { - return replaced; - } - - const instance = Private(Provider); - expect(instance).to.be(replaced); - - Private.stub(Provider, replacement); - - const instance2 = Private(Provider); - expect(instance2).to.be(replacement); - - Private.stub(Provider, replaced); - - const instance3 = Private(Provider); - expect(instance3).to.be(replaced); - }); - }); - - describe('#swap', function () { - it('accepts a new Provider that should replace an existing Provider', function () { - function Provider1() { - return {}; - } - - function Provider2() { - return {}; - } - - const instance1 = Private(Provider1); - expect(instance1).to.be.an('object'); - - Private.swap(Provider1, Provider2); - - const instance2 = Private(Provider1); - expect(instance2).to.be.an('object'); - expect(instance2).to.not.be(instance1); - - Private.swap(Provider1, Provider1); - - const instance3 = Private(Provider1); - expect(instance3).to.be(instance1); - }); - - it('gives the new Provider access to the Provider it replaced via an injectable dependency called $decorate', function () { - function Provider1() { - return {}; - } - - function Provider2($decorate) { - return { - instance1: $decorate(), - }; - } - - const instance1 = Private(Provider1); - expect(instance1).to.be.an('object'); - - Private.swap(Provider1, Provider2); - - const instance2 = Private(Provider1); - expect(instance2).to.have.property('instance1'); - expect(instance2.instance1).to.be(instance1); - }); - }); -}); diff --git a/src/legacy/ui/public/private/index.d.ts b/src/legacy/ui/public/private/index.d.ts deleted file mode 100644 index 3b692ba58cbe1..0000000000000 --- a/src/legacy/ui/public/private/index.d.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 { IPrivate } from '../../../../plugins/kibana_legacy/public/'; diff --git a/src/legacy/ui/public/private/index.js b/src/legacy/ui/public/private/index.js deleted file mode 100644 index 3815f230df466..0000000000000 --- a/src/legacy/ui/public/private/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. - */ - -import './private'; diff --git a/src/legacy/ui/public/private/private.js b/src/legacy/ui/public/private/private.js deleted file mode 100644 index 7a0751959417e..0000000000000 --- a/src/legacy/ui/public/private/private.js +++ /dev/null @@ -1,23 +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 { uiModules } from '../modules'; -import { PrivateProvider } from '../../../../plugins/kibana_legacy/public'; - -uiModules.get('kibana/private').provider('Private', PrivateProvider); diff --git a/src/legacy/ui/public/promises/__tests__/promises.js b/src/legacy/ui/public/promises/__tests__/promises.js deleted file mode 100644 index 7041aa2993376..0000000000000 --- a/src/legacy/ui/public/promises/__tests__/promises.js +++ /dev/null @@ -1,277 +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 ngMock from 'ng_mock'; -import sinon from 'sinon'; - -describe('Promise service', () => { - let Promise; - let $rootScope; - - const sandbox = sinon.createSandbox(); - function tick(ms = 0) { - sandbox.clock.tick(ms); - - // Ugly, but necessary for promises to resolve: https://github.com/angular/angular.js/issues/12555 - $rootScope.$apply(); - } - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(($injector) => { - sandbox.useFakeTimers(); - - Promise = $injector.get('Promise'); - $rootScope = $injector.get('$rootScope'); - }) - ); - - afterEach(() => sandbox.restore()); - - describe('Constructor', () => { - it('provides resolve and reject function', () => { - const executor = sinon.stub(); - new Promise(executor); - - sinon.assert.calledOnce(executor); - sinon.assert.calledWithExactly(executor, sinon.match.func, sinon.match.func); - }); - }); - - it('Promise.resolve', () => { - const onResolve = sinon.stub(); - Promise.resolve(true).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, true); - }); - - describe('Promise.fromNode', () => { - it('creates a callback that controls a promise', () => { - const callback = sinon.stub(); - Promise.fromNode(callback); - - tick(); - - sinon.assert.calledOnce(callback); - sinon.assert.calledWithExactly(callback, sinon.match.func); - }); - - it('rejects if the callback receives an error', () => { - const err = new Error(); - const onReject = sinon.stub(); - Promise.fromNode(sinon.stub().yields(err)).catch(onReject); - - tick(); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(err)); - }); - - it('resolves with the second argument', () => { - const result = {}; - const onResolve = sinon.stub(); - Promise.fromNode(sinon.stub().yields(null, result)).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, sinon.match.same(result)); - }); - - it('resolves with an array if multiple arguments are received', () => { - const result1 = {}; - const result2 = {}; - const onResolve = sinon.stub(); - Promise.fromNode(sinon.stub().yields(null, result1, result2)).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, [ - sinon.match.same(result1), - sinon.match.same(result2), - ]); - }); - - it('resolves with an array if multiple undefined are received', () => { - const onResolve = sinon.stub(); - Promise.fromNode(sinon.stub().yields(null, undefined, undefined)).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, [undefined, undefined]); - }); - }); - - describe('Promise.race()', () => { - it(`resolves with the first resolved promise's value`, () => { - const p1 = new Promise((resolve) => setTimeout(resolve, 100, 1)); - const p2 = new Promise((resolve) => setTimeout(resolve, 200, 2)); - const onResolve = sinon.stub(); - Promise.race([p1, p2]).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - it(`rejects with the first rejected promise's rejection reason`, () => { - const p1Error = new Error('1'); - const p1 = new Promise((r, reject) => setTimeout(reject, 200, p1Error)); - - const p2Error = new Error('2'); - const p2 = new Promise((r, reject) => setTimeout(reject, 100, p2Error)); - - const onReject = sinon.stub(); - Promise.race([p1, p2]).catch(onReject); - - tick(200); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(p2Error)); - }); - - it('does not wait for subsequent promises to resolve/reject', () => { - const onP1Resolve = sinon.stub(); - const p1 = new Promise((resolve) => setTimeout(resolve, 100)).then(onP1Resolve); - - const onP2Resolve = sinon.stub(); - const p2 = new Promise((resolve) => setTimeout(resolve, 101)).then(onP2Resolve); - - const onResolve = sinon.stub(); - Promise.race([p1, p2]).then(onResolve); - - tick(100); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledOnce(onP1Resolve); - sinon.assert.callOrder(onP1Resolve, onResolve); - sinon.assert.notCalled(onP2Resolve); - }); - - it('allows non-promises in the array', () => { - const onResolve = sinon.stub(); - Promise.race([1, 2, 3]).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - describe('argument is undefined', () => { - it('rejects the promise', () => { - const football = {}; - const onReject = sinon.stub(); - Promise.race() - .catch(() => football) - .then(onReject); - - tick(); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(football)); - }); - }); - - describe('argument is a string', () => { - it(`resolves with the first character`, () => { - const onResolve = sinon.stub(); - Promise.race('abc').then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 'a'); - }); - }); - - describe('argument is a non-iterable object', () => { - it('reject the promise', () => { - const football = {}; - const onReject = sinon.stub(); - Promise.race({}) - .catch(() => football) - .then(onReject); - - tick(); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(football)); - }); - }); - - describe('argument is a generator', () => { - it('resolves with the first resolved value', () => { - function* gen() { - yield new Promise((resolve) => setTimeout(resolve, 100, 1)); - yield new Promise((resolve) => setTimeout(resolve, 200, 2)); - } - - const onResolve = sinon.stub(); - Promise.race(gen()).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - it('resolves with the first non-promise value', () => { - function* gen() { - yield 1; - yield new Promise((resolve) => setTimeout(resolve, 200, 2)); - } - - const onResolve = sinon.stub(); - Promise.race(gen()).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - it('iterates all values from the generator, even if one is already "resolved"', () => { - let yieldCount = 0; - function* gen() { - yieldCount += 1; - yield 1; - yieldCount += 1; - yield new Promise((resolve) => setTimeout(resolve, 200, 2)); - } - - const onResolve = sinon.stub(); - Promise.race(gen()).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - expect(yieldCount).to.be(2); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/promises/defer.ts b/src/legacy/ui/public/promises/defer.ts deleted file mode 100644 index 3d435f2ba8dfd..0000000000000 --- a/src/legacy/ui/public/promises/defer.ts +++ /dev/null @@ -1,33 +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 interface Defer { - promise: Promise; - resolve(value: T): void; - reject(reason: Error): void; -} - -export function createDefer(Class: typeof Promise): Defer { - const defer: Partial> = {}; - defer.promise = new Class((resolve, reject) => { - defer.resolve = resolve; - defer.reject = reject; - }); - return defer as Defer; -} diff --git a/src/legacy/ui/public/promises/index.ts b/src/legacy/ui/public/promises/index.ts deleted file mode 100644 index 07bb2554c5eda..0000000000000 --- a/src/legacy/ui/public/promises/index.ts +++ /dev/null @@ -1,28 +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 { PromiseServiceCreator } from '../../../../plugins/kibana_legacy/public'; -export { createDefer } from './defer'; -// @ts-ignore -import { uiModules } from '../modules'; - -const module = uiModules.get('kibana'); -// Provides a tiny subset of the excellent API from -// bluebird, reimplemented using the $q service -module.service('Promise', PromiseServiceCreator); diff --git a/src/legacy/ui/public/react_components.js b/src/legacy/ui/public/react_components.js deleted file mode 100644 index 21fb53d6407fa..0000000000000 --- a/src/legacy/ui/public/react_components.js +++ /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. - */ - -import 'ngreact'; - -import { EuiIcon, EuiIconTip } from '@elastic/eui'; - -import { uiModules } from './modules'; - -const app = uiModules.get('app/kibana', ['react']); - -app.directive('icon', (reactDirective) => reactDirective(EuiIcon)); - -app.directive('iconTip', (reactDirective) => - reactDirective(EuiIconTip, ['content', 'type', 'position', 'title', 'color']) -); diff --git a/src/legacy/ui/public/registry/__tests__/registry.js b/src/legacy/ui/public/registry/__tests__/registry.js deleted file mode 100644 index 10b2423abc0e3..0000000000000 --- a/src/legacy/ui/public/registry/__tests__/registry.js +++ /dev/null @@ -1,168 +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 { uiRegistry } from '../_registry'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -describe('Registry', function () { - let Private; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function ($injector) { - Private = $injector.get('Private'); - }) - ); - - it('is technically a function', function () { - const reg = uiRegistry(); - expect(reg).to.be.a('function'); - }); - - describe('#register', function () { - it('accepts a Private module', function () { - const reg = uiRegistry(); - const mod = function SomePrivateModule() {}; - - reg.register(mod); - // modules are not exposed, so this is the most that we can test - }); - - it('applies the filter function if one is specified', function () { - const reg = uiRegistry({ - filter: (item) => item.value % 2 === 0, // register only even numbers - }); - - reg.register(() => ({ value: 17 })); - reg.register(() => ({ value: 18 })); // only this one should get registered - reg.register(() => ({ value: 19 })); - - const modules = Private(reg); - expect(modules).to.have.length(1); - expect(modules[0].value).to.be(18); - }); - }); - - describe('as a module', function () { - it('exposes the list of registered modules', function () { - const reg = uiRegistry(); - const mod = function SomePrivateModule(Private) { - this.PrivateModuleLoader = Private; - }; - - reg.register(mod); - const modules = Private(reg); - expect(modules).to.have.length(1); - expect(modules[0]).to.have.property('PrivateModuleLoader', Private); - }); - }); - - describe('spec', function () { - it('executes with the module list as "this", and can override it', function () { - let self; - - const reg = uiRegistry({ - constructor: function () { - return { mods: (self = this) }; - }, - }); - - const modules = Private(reg); - expect(modules).to.be.an('object'); - expect(modules).to.have.property('mods', self); - }); - }); - - describe('spec.name', function () { - it('sets the displayName of the registry and the name param on the final instance', function () { - const reg = uiRegistry({ - name: 'visTypes', - }); - - expect(reg).to.have.property('displayName', '[registry visTypes]'); - expect(Private(reg)).to.have.property('name', 'visTypes'); - }); - }); - - describe('spec.constructor', function () { - it('executes before the modules are returned', function () { - let i = 0; - - const reg = uiRegistry({ - constructor: function () { - i = i + 1; - }, - }); - - Private(reg); - expect(i).to.be(1); - }); - - it('executes with the module list as "this", and can override it', function () { - let self; - - const reg = uiRegistry({ - constructor: function () { - return { mods: (self = this) }; - }, - }); - - const modules = Private(reg); - expect(modules).to.be.an('object'); - expect(modules).to.have.property('mods', self); - }); - }); - - describe('spec.invokeProviders', () => { - it('is called with the registered providers and defines the initial set of values in the registry', () => { - const reg = uiRegistry({ - invokeProviders(providers) { - return providers.map((i) => i * 1000); - }, - }); - - reg.register(1); - reg.register(2); - reg.register(3); - expect(Private(reg).toJSON()).to.eql([1000, 2000, 3000]); - }); - it('does not get assigned as a property of the registry', () => { - expect( - uiRegistry({ - invokeProviders() {}, - }) - ).to.not.have.property('invokeProviders'); - }); - }); - - describe('spec[any]', function () { - it('mixes the extra properties into the module list', function () { - const reg = uiRegistry({ - someMethod: function () { - return this; - }, - }); - - const modules = Private(reg); - expect(modules).to.have.property('someMethod'); - expect(modules.someMethod()).to.be(modules); - }); - }); -}); diff --git a/src/legacy/ui/public/registry/_registry.d.ts b/src/legacy/ui/public/registry/_registry.d.ts deleted file mode 100644 index 42f1bb521763c..0000000000000 --- a/src/legacy/ui/public/registry/_registry.d.ts +++ /dev/null @@ -1,40 +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 { IndexedArray, IndexedArrayConfig } from '../indexed_array'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface UIRegistry extends IndexedArray {} - -interface UIRegistrySpec extends IndexedArrayConfig { - name: string; - filter?(item: T): boolean; -} - -/** - * Creates a new UiRegistry (See js method for detailed documentation) - * The generic type T is the type of objects which are stored in the registry. - * The generic type A is an interface of accessors which depend on the - * fields of the objects stored in the registry. - * Example: if there is a string field "name" in type T, then A should be - * `{ byName: { [typeName: string]: T }; }` - */ -declare function uiRegistry( - spec: UIRegistrySpec -): { (): UIRegistry & A; register(privateModule: T): UIRegistry & A }; diff --git a/src/legacy/ui/public/registry/_registry.js b/src/legacy/ui/public/registry/_registry.js deleted file mode 100644 index 85aa1d9f2eca8..0000000000000 --- a/src/legacy/ui/public/registry/_registry.js +++ /dev/null @@ -1,141 +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 _ from 'lodash'; -import { IndexedArray } from '../indexed_array'; - -const notPropsOptNames = IndexedArray.OPT_NAMES.concat('constructor', 'invokeProviders'); - -/** - * Create a registry, which is just a Private module provider. - * - * The registry allows modifying the values it will provide - * using the #register method. - * - * To access these modules, pass the registry to the Private - * module loader. - * - * # Examples - * - * + register a module - * ```js - * let registry = require('ui/registry/vis_types'); - * registry.add(function InjectablePrivateModule($http, Promise) { - * ... - * }) - * ``` - * - * + get all registered modules - * ```js - * let visTypes = Private(RegistryVisTypesProvider); - * ``` - * - * - * @param {object} [spec] - an object describing the properties of - * the registry to create. Any property specified - * that is not listed below will be mixed into the - * final IndexedArray object. - * - * # init - * @param {Function} [spec.constructor] - an injectable function that is called when - * the registry is first instantiated by the app. - * @param {boolean} [spec.filter] - function that will be used to filter items before - * registering them. Function will called on each item and - * should return true to keep the item (register it) or - * skip it (don't register it) - * - * # IndexedArray params - * @param {array[String]} [spec.index] - passed to the IndexedArray constructor - * @param {array[String]} [spec.group] - passed to the IndexedArray constructor - * @param {array[String]} [spec.order] - passed to the IndexedArray constructor - * @param {array[String]} [spec.initialSet] - passed to the IndexedArray constructor - * @param {array[String]} [spec.immutable] - passed to the IndexedArray constructor - * - * @return {[type]} [description] - */ -export function uiRegistry(spec) { - spec = spec || {}; - - const constructor = _.has(spec, 'constructor') && spec.constructor; - const filter = _.has(spec, 'filter') && spec.filter; - const invokeProviders = _.has(spec, 'invokeProviders') && spec.invokeProviders; - const iaOpts = _.defaults(_.pick(spec, IndexedArray.OPT_NAMES), { index: ['name'] }); - const props = _.omit(spec, notPropsOptNames); - const providers = []; - - let isInstantiated = false; - let getInvokedProviders; - let modules; - - /** - * This is the Private module that will be instantiated by - * - * @tag:PrivateModule - * @return {IndexedArray} - an indexed array containing the values - * that were registered, the registry spec - * defines how things will be indexed. - */ - const registry = function (Private, $injector) { - getInvokedProviders = function (newProviders) { - let set = invokeProviders - ? $injector.invoke(invokeProviders, undefined, { providers: newProviders }) - : newProviders.map(Private); - - if (filter && _.isFunction(filter)) { - set = set.filter((item) => filter(item)); - } - - return set; - }; - - iaOpts.initialSet = getInvokedProviders(providers); - - modules = new IndexedArray(iaOpts); - - // mixin other props - _.assign(modules, props); - - // construct - if (constructor) { - modules = $injector.invoke(constructor, modules) || modules; - } - - isInstantiated = true; - - return modules; - }; - - registry.displayName = '[registry ' + props.name + ']'; - - registry.register = function (privateModule) { - providers.push(privateModule); - - if (isInstantiated) { - const [provider] = getInvokedProviders([privateModule]); - - if (provider) { - modules.push(provider); - } - } - - return registry; - }; - - return registry; -} diff --git a/src/legacy/ui/public/registry/chrome_header_nav_controls.ts b/src/legacy/ui/public/registry/chrome_header_nav_controls.ts deleted file mode 100644 index 7e6c80692cd63..0000000000000 --- a/src/legacy/ui/public/registry/chrome_header_nav_controls.ts +++ /dev/null @@ -1,49 +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 { IndexedArray } from '../indexed_array'; -import { uiRegistry, UIRegistry } from './_registry'; - -interface ChromeHeaderNavControlsRegistryAccessors { - bySide: { [typeName: string]: IndexedArray }; -} - -export enum NavControlSide { - Left = 'left', - Right = 'right', -} - -export interface NavControl { - name: string; - order: number; - side: NavControlSide; - render: (targetDomElement: HTMLDivElement) => () => void; -} - -export type ChromeHeaderNavControlsRegistry = UIRegistry & - ChromeHeaderNavControlsRegistryAccessors; - -export const chromeHeaderNavControlsRegistry = uiRegistry< - NavControl, - ChromeHeaderNavControlsRegistryAccessors ->({ - name: 'chromeHeaderNavControls', - order: ['order'], - group: ['side'], -}); diff --git a/src/legacy/ui/public/registry/field_format_editors.ts b/src/legacy/ui/public/registry/field_format_editors.ts deleted file mode 100644 index 5489ce9250e04..0000000000000 --- a/src/legacy/ui/public/registry/field_format_editors.ts +++ /dev/null @@ -1,25 +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 { uiRegistry } from './_registry'; - -export const RegistryFieldFormatEditorsProvider = uiRegistry({ - name: 'fieldFormatEditors', - index: ['formatId'], -}); diff --git a/src/legacy/ui/public/routes/__tests__/_route_manager.js b/src/legacy/ui/public/routes/__tests__/_route_manager.js deleted file mode 100644 index eb47a3e9ace70..0000000000000 --- a/src/legacy/ui/public/routes/__tests__/_route_manager.js +++ /dev/null @@ -1,218 +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 _ from 'lodash'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; -import RouteManager from '../route_manager'; -import expect from '@kbn/expect'; - -let routes; // will contain an new instance of RouteManager for each test -const chainableMethods = [ - { name: 'when', args: ['', {}] }, - { name: 'otherwise', args: [{}] }, - { name: 'defaults', args: [/regexp/, {}] }, -]; - -let $rp; -describe('routes/route_manager', function () { - beforeEach( - ngMock.module('kibana', function ($routeProvider) { - $rp = $routeProvider; - sinon.stub($rp, 'otherwise'); - sinon.stub($rp, 'when'); - }) - ); - - beforeEach( - ngMock.inject(function () { - routes = new RouteManager(); - }) - ); - - it('should have chainable methods: ' + _.map(chainableMethods, 'name').join(', '), function () { - chainableMethods.forEach(function (meth) { - expect(routes[meth.name].apply(routes, _.clone(meth.args))).to.be(routes); - }); - }); - - describe('#otherwise', function () { - it('should forward the last otherwise route', function () { - const otherRoute = {}; - routes.otherwise({}); - routes.otherwise(otherRoute); - - routes.config($rp); - - expect($rp.otherwise.callCount).to.be(1); - expect($rp.otherwise.getCall(0).args[0]).to.be(otherRoute); - }); - }); - - describe('#when', function () { - it('should merge the additions into the when() defined routes', function () { - routes.when('/some/route'); - routes.when('/some/other/route'); - - // add the addition resolve to every route - routes.defaults(/.*/, { - resolve: { - addition: function () {}, - }, - }); - - routes.config($rp); - - // should have run once for each when route - expect($rp.when.callCount).to.be(2); - expect($rp.otherwise.callCount).to.be(0); - - // every route should have the "addition" resolve - expect($rp.when.getCall(0).args[1].resolve.addition).to.be.a('function'); - expect($rp.when.getCall(1).args[1].resolve.addition).to.be.a('function'); - }); - }); - - describe('#config', function () { - it('should add defined routes to the global $routeProvider service in order', function () { - const args = [ - ['/one', {}], - ['/two', {}], - ]; - - args.forEach(function (a) { - routes.when(a[0], a[1]); - }); - - routes.config($rp); - - expect($rp.when.callCount).to.be(args.length); - _.times(args.length, function (i) { - const call = $rp.when.getCall(i); - const a = args.shift(); - - expect(call.args[0]).to.be(a[0]); - expect(call.args[1]).to.be(a[1]); - }); - }); - - it('sets route.reloadOnSearch to false by default', function () { - routes.when('/nothing-set'); - routes.when('/no-reload', { reloadOnSearch: false }); - routes.when('/always-reload', { reloadOnSearch: true }); - routes.config($rp); - - expect($rp.when.callCount).to.be(3); - expect($rp.when.firstCall.args[1]).to.have.property('reloadOnSearch', false); - expect($rp.when.secondCall.args[1]).to.have.property('reloadOnSearch', false); - expect($rp.when.lastCall.args[1]).to.have.property('reloadOnSearch', true); - }); - }); - - describe('#defaults()', () => { - it('adds defaults to routes with matching paths', () => { - routes.when('/foo', { name: 'foo' }); - routes.when('/bar', { name: 'bar' }); - routes.when('/baz', { name: 'baz' }); - routes.defaults(/^\/ba/, { - withDefaults: true, - }); - routes.config($rp); - - sinon.assert.calledWithExactly( - $rp.when, - '/foo', - sinon.match({ name: 'foo', withDefaults: undefined }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/bar', - sinon.match({ name: 'bar', withDefaults: true }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/baz', - sinon.match({ name: 'baz', withDefaults: true }) - ); - }); - - it('does not override values specified in the route', () => { - routes.when('/foo', { name: 'foo' }); - routes.defaults(/./, { name: 'bar' }); - routes.config($rp); - - sinon.assert.calledWithExactly($rp.when, '/foo', sinon.match({ name: 'foo' })); - }); - - // See https://github.com/elastic/kibana/issues/13294 - it('does not assign defaults by reference, to prevent accidentally merging unrelated defaults together', () => { - routes.when('/foo', { name: 'foo' }); - routes.when('/bar', { name: 'bar' }); - routes.when('/baz', { name: 'baz', funcs: { bazFunc() {} } }); - - // multiple defaults must be defined that, when applied correctly, will - // create a new object property on all routes that is unique to all of them - routes.defaults(/./, { funcs: { all() {} } }); - routes.defaults(/^\/foo/, { funcs: { fooFunc() {} } }); - routes.defaults(/^\/bar/, { funcs: { barFunc() {} } }); - routes.config($rp); - - sinon.assert.calledThrice($rp.when); - sinon.assert.calledWithExactly( - $rp.when, - '/foo', - sinon.match({ - name: 'foo', - funcs: sinon.match({ - all: sinon.match.func, - fooFunc: sinon.match.func, - barFunc: undefined, - bazFunc: undefined, - }), - }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/bar', - sinon.match({ - name: 'bar', - funcs: sinon.match({ - all: sinon.match.func, - fooFunc: undefined, - barFunc: sinon.match.func, - bazFunc: undefined, - }), - }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/baz', - sinon.match({ - name: 'baz', - funcs: sinon.match({ - all: sinon.match.func, - fooFunc: undefined, - barFunc: undefined, - bazFunc: sinon.match.func, - }), - }) - ); - }); - }); -}); diff --git a/src/legacy/ui/public/routes/__tests__/_work_queue.js b/src/legacy/ui/public/routes/__tests__/_work_queue.js deleted file mode 100644 index 72891f7321fbd..0000000000000 --- a/src/legacy/ui/public/routes/__tests__/_work_queue.js +++ /dev/null @@ -1,116 +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 _ from 'lodash'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { WorkQueue } from '../work_queue'; -import sinon from 'sinon'; -import { createDefer } from 'ui/promises'; - -describe('work queue', function () { - let queue; - let Promise; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_Promise_) { - Promise = _Promise_; - }) - ); - beforeEach(function () { - queue = new WorkQueue(); - }); - afterEach(function () { - queue.empty(); - }); - - describe('#push', function () { - it('adds to the interval queue', function () { - queue.push(createDefer(Promise)); - expect(queue).to.have.length(1); - }); - }); - - describe('#resolveWhenFull', function () { - it('resolves requests waiting for the queue to fill when appropriate', function () { - const size = _.random(5, 50); - queue.limit = size; - - const whenFull = createDefer(Promise); - sinon.stub(whenFull, 'resolve'); - queue.resolveWhenFull(whenFull); - - // push all but one into the queue - _.times(size - 1, function () { - queue.push(createDefer(Promise)); - }); - - expect(whenFull.resolve.callCount).to.be(0); - queue.push(createDefer(Promise)); - expect(whenFull.resolve.callCount).to.be(1); - - queue.empty(); - }); - }); - - /** - * Fills the queue with a random number of work defers, but stubs all defer methods - * with the same stub and passed it back. - * - * @param {function} then - called with then(size, stub) so that the test - * can manipulate the filled queue - */ - function fillWithStubs(then) { - const size = _.random(5, 50); - const stub = sinon.stub(); - - _.times(size, function () { - const d = createDefer(Promise); - // overwrite the defer methods with the stub - d.resolve = stub; - d.reject = stub; - queue.push(d); - }); - - then(size, stub); - } - - describe('#doWork', function () { - it('flushes the queue and resolves all promises', function () { - fillWithStubs(function (size, stub) { - expect(queue).to.have.length(size); - queue.doWork(); - expect(queue).to.have.length(0); - expect(stub.callCount).to.be(size); - }); - }); - }); - - describe('#empty()', function () { - it('empties the internal queue WITHOUT resolving any promises', function () { - fillWithStubs(function (size, stub) { - expect(queue).to.have.length(size); - queue.empty(); - expect(queue).to.have.length(0); - expect(stub.callCount).to.be(0); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js b/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js deleted file mode 100644 index 8ae85fce591a1..0000000000000 --- a/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js +++ /dev/null @@ -1,106 +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 RouteManager from '../route_manager'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import _ from 'lodash'; -import '../../private'; - -let routes; - -describe('wrapRouteWithPrep fn', function () { - require('test_utils/no_digest_promises').activateForSuite(); - - beforeEach(function () { - routes = new RouteManager(); - }); - - const SchedulingTest = function (opts) { - opts = opts || {}; - - const delaySetup = opts.delayUserWork ? 0 : 50; - const delayUserWork = opts.delayUserWork ? 50 : 0; - - return function () { - ngMock.module('kibana'); - let setupComplete = false; - let userWorkComplete = false; - let route; - let Promise; - let $injector; - - ngMock.inject(function (_Promise_, _$injector_) { - Promise = _Promise_; - $injector = _$injector_; - }); - - routes.addSetupWork(function () { - return new Promise(function (resolve) { - setTimeout(function () { - setupComplete = true; - resolve(); - }, delaySetup); - }); - }); - - routes - .when('/', { - resolve: { - test: function () { - expect(setupComplete).to.be(true); - userWorkComplete = true; - }, - }, - }) - .config({ - when: function (p, _r) { - route = _r; - }, - }); - - return new Promise(function (resolve, reject) { - setTimeout(function () { - Promise.all( - _.map(route.resolve, function (fn) { - return $injector.invoke(fn); - }) - ) - .then(function () { - expect(setupComplete).to.be(true); - expect(userWorkComplete).to.be(true); - }) - .then(resolve, reject); - }, delayUserWork); - }); - }; - }; - - it('always waits for setup to complete before calling user work', new SchedulingTest()); - - it('does not call user work when setup fails', new SchedulingTest({ failSetup: true })); - - it( - 'calls all user work even if it is not initialized until after setup is complete', - new SchedulingTest({ - delayUserWork: false, - }) - ); -}); diff --git a/src/legacy/ui/public/routes/__tests__/index.js b/src/legacy/ui/public/routes/__tests__/index.js deleted file mode 100644 index 30cedaaca3d19..0000000000000 --- a/src/legacy/ui/public/routes/__tests__/index.js +++ /dev/null @@ -1,23 +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 './_route_manager'; -import './_work_queue'; -import './_wrap_route_with_prep'; -describe('Custom Route Management', function () {}); diff --git a/src/legacy/ui/public/routes/breadcrumbs.js b/src/legacy/ui/public/routes/breadcrumbs.js deleted file mode 100644 index 7917ffbd7c6e6..0000000000000 --- a/src/legacy/ui/public/routes/breadcrumbs.js +++ /dev/null @@ -1,44 +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 { trim, startCase } from 'lodash'; - -/** - * Take a path (from $location.path() usually) and parse - * it's segments into a list of breadcrumbs - * - * @param {string} path - * @return {Array} - */ -export function parsePathToBreadcrumbs(path) { - return trim(path, '/') - .split('/') - .reduce( - (acc, id, i, parts) => [ - ...acc, - { - id, - display: startCase(id), - href: i === 0 ? `#/${id}` : `${acc[i - 1].href}/${id}`, - current: i === parts.length - 1, - }, - ], - [] - ); -} diff --git a/src/legacy/ui/public/routes/index.d.ts b/src/legacy/ui/public/routes/index.d.ts deleted file mode 100644 index 84e17b0a69452..0000000000000 --- a/src/legacy/ui/public/routes/index.d.ts +++ /dev/null @@ -1,24 +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 { uiRoutes, UIRoutes } from 'ui/routes/routes'; - -// eslint-disable-next-line import/no-default-export -export default uiRoutes; -export { UIRoutes }; diff --git a/src/legacy/ui/public/routes/index.js b/src/legacy/ui/public/routes/index.js deleted file mode 100644 index 9c826ebee1230..0000000000000 --- a/src/legacy/ui/public/routes/index.js +++ /dev/null @@ -1,23 +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 { uiRoutes } from './routes'; - -// eslint-disable-next-line import/no-default-export -export default uiRoutes; diff --git a/src/legacy/ui/public/routes/route_manager.d.ts b/src/legacy/ui/public/routes/route_manager.d.ts deleted file mode 100644 index a5261a7c8ee3a..0000000000000 --- a/src/legacy/ui/public/routes/route_manager.d.ts +++ /dev/null @@ -1,47 +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. - */ - -/** - * WARNING: these types are incomplete - */ - -import { ChromeBreadcrumb } from '../../../../core/public'; - -export interface RouteConfiguration { - controller?: string | ((...args: any[]) => void); - redirectTo?: string; - resolveRedirectTo?: (...args: any[]) => void; - reloadOnSearch?: boolean; - reloadOnUrl?: boolean; - outerAngularWrapperRoute?: boolean; - resolve?: object; - template?: string; - k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; - requireUICapability?: string; -} - -interface RouteManager { - addSetupWork(cb: (...args: any[]) => void): void; - when(path: string, routeConfiguration: RouteConfiguration): RouteManager; - otherwise(routeConfiguration: RouteConfiguration): RouteManager; - defaults(path: string | RegExp, defaults: RouteConfiguration): RouteManager; -} - -// eslint-disable-next-line import/no-default-export -export default RouteManager; diff --git a/src/legacy/ui/public/routes/route_manager.js b/src/legacy/ui/public/routes/route_manager.js deleted file mode 100644 index de8a541d1c50a..0000000000000 --- a/src/legacy/ui/public/routes/route_manager.js +++ /dev/null @@ -1,115 +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 { cloneDeep, defaultsDeep, wrap } from 'lodash'; - -import { wrapRouteWithPrep } from './wrap_route_with_prep'; -import { RouteSetupManager } from './route_setup_manager'; -import { parsePathToBreadcrumbs } from './breadcrumbs'; - -// eslint-disable-next-line import/no-default-export -export default function RouteManager() { - const self = this; - const setup = new RouteSetupManager(); - const when = []; - const defaults = []; - let otherwise; - - self.config = function ($routeProvider) { - when.forEach(function (args) { - const path = args[0]; - const route = args[1] || {}; - - defaults.forEach((def) => { - if (def.regex.test(path)) { - defaultsDeep(route, cloneDeep(def.value)); - } - }); - - if (route.reloadOnSearch == null) { - route.reloadOnSearch = false; - } - - wrapRouteWithPrep(route, setup); - $routeProvider.when(path, route); - }); - - if (otherwise) { - wrapRouteWithPrep(otherwise, setup); - $routeProvider.otherwise(otherwise); - } - }; - - self.run = function ($location, $route, $injector, $rootScope) { - if (window.elasticApm && typeof window.elasticApm.startTransaction === 'function') { - /** - * capture route-change events as transactions which happens after - * the browser's on load event. - * - * In Kibana app, this logic would run after the boostrap js files gets - * downloaded and get associated with the page-load transaction - */ - $rootScope.$on('$routeChangeStart', (_, nextRoute) => { - if (nextRoute.$$route) { - const name = nextRoute.$$route.originalPath; - window.elasticApm.startTransaction(name, 'route-change'); - } - }); - } - - self.getBreadcrumbs = () => { - const breadcrumbs = parsePathToBreadcrumbs($location.path()); - const map = $route.current.mapBreadcrumbs; - return map ? $injector.invoke(map, null, { breadcrumbs }) : breadcrumbs; - }; - }; - - const wrapSetupAndChain = (fn, ...args) => { - fn.apply(setup, args); - return this; - }; - - this.addSetupWork = wrap(setup.addSetupWork, wrapSetupAndChain); - this.afterSetupWork = wrap(setup.afterSetupWork, wrapSetupAndChain); - this.afterWork = wrap(setup.afterWork, wrapSetupAndChain); - - self.when = function (path, route) { - when.push([path, route]); - return self; - }; - - // before attaching the routes to the routeProvider, test the RE - // against the .when() path and add/override the resolves if there is a match - self.defaults = function (regex, value) { - defaults.push({ regex, value }); - return self; - }; - - self.otherwise = function (route) { - otherwise = route; - return self; - }; - - self.getBreadcrumbs = function () { - // overwritten in self.run(); - return []; - }; - - self.RouteManager = RouteManager; -} diff --git a/src/legacy/ui/public/routes/route_setup_manager.js b/src/legacy/ui/public/routes/route_setup_manager.js deleted file mode 100644 index a7a2f078f40fb..0000000000000 --- a/src/legacy/ui/public/routes/route_setup_manager.js +++ /dev/null @@ -1,112 +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 _ from 'lodash'; -import { createDefer } from 'ui/promises'; - -// Throw this inside of an Angular route resolver after calling `kbnUrl.change` -// so that the $router can observe the $location update. Otherwise, the location -// route setup work will resolve before the route update occurs. -export const WAIT_FOR_URL_CHANGE_TOKEN = new Error('WAIT_FOR_URL_CHANGE_TOKEN'); - -export class RouteSetupManager { - constructor() { - this.setupWork = []; - this.onSetupComplete = []; - this.onSetupError = []; - this.onWorkComplete = []; - this.onWorkError = []; - } - - addSetupWork(fn) { - this.setupWork.push(fn); - } - - afterSetupWork(onComplete, onError) { - this.onSetupComplete.push(onComplete); - this.onSetupError.push(onError); - } - - afterWork(onComplete, onError) { - this.onWorkComplete.push(onComplete); - this.onWorkError.push(onError); - } - - /** - * Do each setupWork function by injecting it with angular dependencies - * and accepting promises from it. - * @return {[type]} [description] - */ - doWork(Promise, $injector, userWork) { - const invokeEach = (arr, locals) => { - return Promise.map(arr, (fn) => { - if (!fn) return; - return $injector.invoke(fn, null, locals); - }); - }; - - // call each error handler in order, until one of them resolves - // or we run out of handlers - const callErrorHandlers = (handlers, origError) => { - if (!_.size(handlers)) throw origError; - - // clone so we don't discard handlers or loose them - handlers = handlers.slice(0); - - const next = (err) => { - if (!handlers.length) throw err; - - const handler = handlers.shift(); - if (!handler) return next(err); - - return Promise.try(function () { - return $injector.invoke(handler, null, { err }); - }).catch(next); - }; - - return next(origError); - }; - - return invokeEach(this.setupWork) - .then( - () => invokeEach(this.onSetupComplete), - (err) => callErrorHandlers(this.onSetupError, err) - ) - .then(() => { - // wait for the queue to fill up, then do all the work - const defer = createDefer(Promise); - userWork.resolveWhenFull(defer); - - return defer.promise.then(() => Promise.all(userWork.doWork())); - }) - .catch((error) => { - if (error === WAIT_FOR_URL_CHANGE_TOKEN) { - // prevent moving forward, return a promise that never resolves - // so that the $router can observe the $location update - return Promise.halt(); - } - - throw error; - }) - .then( - () => invokeEach(this.onWorkComplete), - (err) => callErrorHandlers(this.onWorkError, err) - ); - } -} diff --git a/src/legacy/ui/public/routes/routes.d.ts b/src/legacy/ui/public/routes/routes.d.ts deleted file mode 100644 index d48230e9d56f9..0000000000000 --- a/src/legacy/ui/public/routes/routes.d.ts +++ /dev/null @@ -1,28 +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 RouteManager from 'ui/routes/route_manager'; - -interface DefaultRouteManager extends RouteManager { - WAIT_FOR_URL_CHANGE_TOKEN: string; - enable(): void; -} - -export const uiRoutes: DefaultRouteManager; -export type UIRoutes = DefaultRouteManager; diff --git a/src/legacy/ui/public/routes/routes.js b/src/legacy/ui/public/routes/routes.js deleted file mode 100644 index 2cfb7a608e853..0000000000000 --- a/src/legacy/ui/public/routes/routes.js +++ /dev/null @@ -1,39 +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 RouteManager from './route_manager'; -import 'angular-route/angular-route'; -import { uiModules } from '../modules'; -import { WAIT_FOR_URL_CHANGE_TOKEN } from './route_setup_manager'; -const defaultRouteManager = new RouteManager(); - -export const uiRoutes = Object.create(defaultRouteManager, { - WAIT_FOR_URL_CHANGE_TOKEN: { - value: WAIT_FOR_URL_CHANGE_TOKEN, - }, - - enable: { - value() { - uiModules - .get('kibana', ['ngRoute']) - .config(defaultRouteManager.config) - .run(defaultRouteManager.run); - }, - }, -}); diff --git a/src/legacy/ui/public/routes/work_queue.js b/src/legacy/ui/public/routes/work_queue.js deleted file mode 100644 index 5c4c83663c590..0000000000000 --- a/src/legacy/ui/public/routes/work_queue.js +++ /dev/null @@ -1,65 +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 WorkQueue() { - const q = this; - - const work = []; - const fullDefers = []; - - q.limit = 0; - Object.defineProperty(q, 'length', { - get: function () { - return work.length; - }, - }); - - const resolve = function (defers) { - return defers.splice(0).map(function (defer) { - return defer.resolve(); - }); - }; - - const checkIfFull = function () { - if (work.length >= q.limit && fullDefers.length) { - resolve(fullDefers); - } - }; - - q.resolveWhenFull = function (defer) { - fullDefers.push(defer); - checkIfFull(); - }; - - q.doWork = function () { - const resps = resolve(work); - checkIfFull(); - return resps; - }; - - q.empty = function () { - work.splice(0); - checkIfFull(); - }; - - q.push = function (defer) { - work.push(defer); - checkIfFull(); - }; -} diff --git a/src/legacy/ui/public/routes/wrap_route_with_prep.js b/src/legacy/ui/public/routes/wrap_route_with_prep.js deleted file mode 100644 index e9ed33148d9ac..0000000000000 --- a/src/legacy/ui/public/routes/wrap_route_with_prep.js +++ /dev/null @@ -1,52 +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 _ from 'lodash'; -import { createDefer } from 'ui/promises'; -import { WorkQueue } from './work_queue'; - -export function wrapRouteWithPrep(route, setup) { - if (!route.resolve && route.redirectTo) return; - - const userWork = new WorkQueue(); - // the point at which we will consider the queue "full" - userWork.limit = _.keys(route.resolve).length; - - const resolve = { - __prep__: function ($injector) { - return $injector.invoke(setup.doWork, setup, { userWork }); - }, - }; - - // send each user resolve to the userWork queue, which will prevent it from running before the - // prep is complete - _.forOwn(route.resolve || {}, function (expr, name) { - resolve[name] = function ($injector, Promise) { - const defer = createDefer(Promise); - userWork.push(defer); - return defer.promise.then(function () { - return $injector[angular.isString(expr) ? 'get' : 'invoke'](expr); - }); - }; - }); - - // we're copied everything over so now overwrite - route.resolve = resolve; -} diff --git a/src/legacy/ui/public/state_management/__tests__/app_state.js b/src/legacy/ui/public/state_management/__tests__/app_state.js deleted file mode 100644 index c47fa3bb0417f..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/app_state.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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { AppStateProvider } from '../app_state'; - -describe('State Management', function () { - let $rootScope; - let AppState; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_$rootScope_, _$location_, Private) { - $rootScope = _$rootScope_; - AppState = Private(AppStateProvider); - }) - ); - - describe('App State', function () { - let appState; - - beforeEach(function () { - appState = new AppState(); - }); - - it('should have _urlParam of _a', function () { - expect(appState).to.have.property('_urlParam'); - expect(appState._urlParam).to.equal('_a'); - }); - - it('should use passed in params', function () { - const params = { - test: true, - mock: false, - }; - - appState = new AppState(params); - expect(appState).to.have.property('_defaults'); - - Object.keys(params).forEach(function (key) { - expect(appState._defaults).to.have.property(key); - expect(appState._defaults[key]).to.equal(params[key]); - }); - }); - - it('should have a destroy method', function () { - expect(appState).to.have.property('destroy'); - }); - - it('should be destroyed on $routeChangeStart', function () { - const destroySpy = sinon.spy(appState, 'destroy'); - - $rootScope.$emit('$routeChangeStart'); - - expect(destroySpy.callCount).to.be(1); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/config_provider.js b/src/legacy/ui/public/state_management/__tests__/config_provider.js deleted file mode 100644 index 9f756bc51dcb0..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/config_provider.js +++ /dev/null @@ -1,57 +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 ngMock from 'ng_mock'; -import '../config_provider'; - -describe('State Management Config', function () { - let stateManagementConfig; - - describe('is enabled', () => { - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_stateManagementConfig_) { - stateManagementConfig = _stateManagementConfig_; - }) - ); - - it('should be enabled by default', () => { - expect(stateManagementConfig.enabled).to.be(true); - }); - }); - - describe('can be disabled', () => { - beforeEach( - ngMock.module('kibana', function (stateManagementConfigProvider) { - stateManagementConfigProvider.disable(); - }) - ); - - beforeEach( - ngMock.inject(function (_stateManagementConfig_) { - stateManagementConfig = _stateManagementConfig_; - }) - ); - - it('is disabled by config', () => { - expect(stateManagementConfig.enabled).to.be(false); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/global_state.js b/src/legacy/ui/public/state_management/__tests__/global_state.js deleted file mode 100644 index e9dae5880a8d1..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/global_state.js +++ /dev/null @@ -1,52 +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 ngMock from 'ng_mock'; -import '../global_state'; - -describe('State Management', function () { - let $location; - let state; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_$location_, globalState) { - $location = _$location_; - state = globalState; - }) - ); - - describe('Global State', function () { - it('should use previous state when not in URL', function () { - // set satte via URL - $location.search({ _g: '(foo:(bar:baz))' }); - state.fetch(); - expect(state.toObject()).to.eql({ foo: { bar: 'baz' } }); - - $location.search({ _g: '(fizz:buzz)' }); - state.fetch(); - expect(state.toObject()).to.eql({ fizz: 'buzz' }); - - $location.search({}); - state.fetch(); - expect(state.toObject()).to.eql({ fizz: 'buzz' }); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/state.js b/src/legacy/ui/public/state_management/__tests__/state.js deleted file mode 100644 index b6c705e814509..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/state.js +++ /dev/null @@ -1,393 +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 ngMock from 'ng_mock'; -import { encode as encodeRison } from 'rison-node'; -import uiRoutes from 'ui/routes'; -import '../../private'; -import { toastNotifications } from '../../notify'; -import * as FatalErrorNS from '../../notify/fatal_error'; -import { StateProvider } from '../state'; -import { - unhashQuery, - createStateHash, - isStateHash, - HashedItemStore, -} from '../../../../../plugins/kibana_utils/public'; -import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { EventsProvider } from '../../events'; - -describe('State Management', () => { - const sandbox = sinon.createSandbox(); - afterEach(() => sandbox.restore()); - - uiRoutes.enable(); - - describe('Enabled', () => { - let $rootScope; - let $location; - let Events; - let setup; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_$rootScope_, _$location_, Private, config) { - const State = Private(StateProvider); - $location = _$location_; - $rootScope = _$rootScope_; - Events = Private(EventsProvider); - - setup = (opts) => { - const { param, initial, storeInHash } = opts || {}; - sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(!!storeInHash); - const store = new StubBrowserStorage(); - const hashedItemStore = new HashedItemStore(store); - const state = new State(param, initial, hashedItemStore); - - const getUnhashedSearch = () => unhashQuery($location.search()); - - return { store, hashedItemStore, state, getUnhashedSearch }; - }; - }) - ); - - describe('Provider', () => { - it('should reset the state to the defaults', () => { - const { state, getUnhashedSearch } = setup({ initial: { message: ['test'] } }); - state.reset(); - const search = getUnhashedSearch(state); - expect(search).to.have.property('_s'); - expect(search._s).to.equal('(message:!(test))'); - expect(state.message).to.eql(['test']); - }); - - it('should apply the defaults upon initialization', () => { - const { state } = setup({ initial: { message: 'test' } }); - expect(state).to.have.property('message', 'test'); - }); - - it('should inherit from Events', () => { - const { state } = setup(); - expect(state).to.be.an(Events); - }); - - it('should emit an event if reset with changes', (done) => { - const { state } = setup({ initial: { message: ['test'] } }); - state.on('reset_with_changes', (keys) => { - expect(keys).to.eql(['message']); - done(); - }); - state.save(); - state.message = 'foo'; - state.reset(); - $rootScope.$apply(); - }); - - it('should not emit an event if reset without changes', () => { - const { state } = setup({ initial: { message: 'test' } }); - state.on('reset_with_changes', () => { - expect().fail(); - }); - state.save(); - state.message = 'test'; - state.reset(); - $rootScope.$apply(); - }); - }); - - describe('Search', () => { - it('should save to $location.search()', () => { - const { state, getUnhashedSearch } = setup({ initial: { test: 'foo' } }); - state.save(); - const search = getUnhashedSearch(state); - expect(search).to.have.property('_s'); - expect(search._s).to.equal('(test:foo)'); - }); - - it('should emit an event if changes are saved', (done) => { - const { state, getUnhashedSearch } = setup(); - state.on('save_with_changes', (keys) => { - expect(keys).to.eql(['test']); - done(); - }); - state.test = 'foo'; - state.save(); - getUnhashedSearch(state); - $rootScope.$apply(); - }); - }); - - describe('Fetch', () => { - it('should emit an event if changes are fetched', (done) => { - const { state } = setup(); - state.on('fetch_with_changes', (keys) => { - expect(keys).to.eql(['foo']); - done(); - }); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - expect(state).to.have.property('foo', 'bar'); - $rootScope.$apply(); - }); - - it('should have events that attach to scope', (done) => { - const { state } = setup(); - state.on('test', (message) => { - expect(message).to.equal('foo'); - done(); - }); - state.emit('test', 'foo'); - $rootScope.$apply(); - }); - - it('should fire listeners for #onUpdate() on #fetch()', (done) => { - const { state } = setup(); - state.on('fetch_with_changes', (keys) => { - expect(keys).to.eql(['foo']); - done(); - }); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - expect(state).to.have.property('foo', 'bar'); - $rootScope.$apply(); - }); - - it('should apply defaults to fetches', () => { - const { state } = setup({ initial: { message: 'test' } }); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - expect(state).to.have.property('foo', 'bar'); - expect(state).to.have.property('message', 'test'); - }); - - it('should call fetch when $routeUpdate is fired on $rootScope', () => { - const { state } = setup(); - const spy = sinon.spy(state, 'fetch'); - $rootScope.$emit('$routeUpdate', 'test'); - sinon.assert.calledOnce(spy); - }); - - it('should clear state when missing form URL', () => { - let stateObj; - const { state } = setup(); - - // set state via URL - $location.search({ _s: '(foo:(bar:baz))' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({ foo: { bar: 'baz' } }); - - // ensure changing URL changes state - $location.search({ _s: '(one:two)' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({ one: 'two' }); - - // remove search, state should be empty - $location.search({}); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - }); - - it('should clear state when it is invalid', () => { - let stateObj; - const { state } = setup(); - - $location.search({ _s: '' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - - $location.search({ _s: '!n' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - - $location.search({ _s: 'alert(1)' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - }); - - it('does not replace the state value on read', () => { - const { state } = setup(); - sinon.stub($location, 'search').callsFake((newSearch) => { - if (newSearch) { - return $location; - } else { - return { - [state.getQueryParamName()]: '(a:1)', - }; - } - }); - const replaceStub = sinon.stub($location, 'replace').returns($location); - - state.fetch(); - sinon.assert.notCalled(replaceStub); - }); - }); - - describe('Hashing', () => { - it('stores state values in a hashedItemStore, writing the hash to the url', () => { - const { state, hashedItemStore } = setup({ storeInHash: true }); - state.foo = 'bar'; - state.save(); - const urlVal = $location.search()[state.getQueryParamName()]; - - expect(isStateHash(urlVal)).to.be(true); - expect(hashedItemStore.getItem(urlVal)).to.eql(JSON.stringify({ foo: 'bar' })); - }); - - it('should replace rison in the URL with a hash', () => { - const { state, hashedItemStore } = setup({ storeInHash: true }); - const obj = { foo: { bar: 'baz' } }; - const rison = encodeRison(obj); - - $location.search({ _s: rison }); - state.fetch(); - - const urlVal = $location.search()._s; - expect(urlVal).to.not.be(rison); - expect(isStateHash(urlVal)).to.be(true); - expect(hashedItemStore.getItem(urlVal)).to.eql(JSON.stringify(obj)); - }); - - describe('error handling', () => { - it('notifies the user when a hash value does not map to a stored value', () => { - // Ideally, state.js shouldn't be tightly coupled to toastNotifications. Instead, it - // should notify its consumer of this error state and the consumer should be responsible - // for notifying the user of the error. This test verifies the side effect of the error - // until we can remove this coupling. - - // Clear existing toasts. - toastNotifications.list.splice(0); - - const { state } = setup({ storeInHash: true }); - const search = $location.search(); - const badHash = createStateHash('{"a": "b"}', () => null); - - search[state.getQueryParamName()] = badHash; - $location.search(search); - - expect(toastNotifications.list).to.have.length(0); - state.fetch(); - expect(toastNotifications.list).to.have.length(1); - expect(toastNotifications.list[0].title).to.match(/use the share functionality/i); - }); - - it.skip('triggers fatal error linking to github when setting item fails', () => { - // NOTE: this test needs to be written in jest and removed from the browser ones - // More info could be read in the opened issue: - // https://github.com/elastic/kibana/issues/22751 - const { state, hashedItemStore } = setup({ storeInHash: true }); - const fatalErrorStub = sandbox.stub(); - Object.defineProperty(FatalErrorNS, 'fatalError', { - writable: true, - value: fatalErrorStub, - }); - - sandbox.stub(hashedItemStore, 'setItem').returns(false); - state.toQueryParam(); - sinon.assert.calledOnce(fatalErrorStub); - sinon.assert.calledWith( - fatalErrorStub, - sinon.match((error) => error instanceof Error && error.message.includes('github.com')) - ); - }); - - it('translateHashToRison should gracefully fallback if parameter can not be parsed', () => { - const { state, hashedItemStore } = setup({ storeInHash: false }); - - expect(state.translateHashToRison('(a:b)')).to.be('(a:b)'); - expect(state.translateHashToRison('')).to.be(''); - - const existingHash = createStateHash('{"a": "b"}', () => null); - hashedItemStore.setItem(existingHash, '{"a": "b"}'); - - const nonExistingHash = createStateHash('{"b": "c"}', () => null); - - expect(state.translateHashToRison(existingHash)).to.be('(a:b)'); - expect(state.translateHashToRison(nonExistingHash)).to.be('!n'); - }); - }); - }); - }); - - describe('Disabled with persisted state', () => { - let state; - let $location; - let $rootScope; - const stateParam = '_config_test'; - - const getLocationState = () => { - const search = $location.search(); - return search[stateParam]; - }; - - beforeEach( - ngMock.module('kibana', function (stateManagementConfigProvider) { - stateManagementConfigProvider.disable(); - }) - ); - beforeEach( - ngMock.inject(function (_$rootScope_, _$location_, Private, config) { - const State = Private(StateProvider); - $location = _$location_; - $rootScope = _$rootScope_; - - sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(false); - - class MockPersistedState extends State { - _persistAcrossApps = true; - } - - MockPersistedState.prototype._persistAcrossApps = true; - - state = new MockPersistedState(stateParam); - }) - ); - - describe('changing state', () => { - const methods = ['save', 'replace', 'reset']; - - methods.forEach((method) => { - it(`${method} should not change the URL`, () => { - $location.search({ _s: '(foo:bar)' }); - state[method](); - $rootScope.$apply(); - expect(getLocationState()).to.be(undefined); - }); - }); - }); - - describe('reading state', () => { - it('should not change the URL', () => { - const saveSpy = sinon.spy(state, 'save'); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - $rootScope.$apply(); - sinon.assert.notCalled(saveSpy); - expect(getLocationState()).to.be(undefined); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js b/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js deleted file mode 100644 index dc00d4e05e82f..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js +++ /dev/null @@ -1,265 +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 { EventEmitter } from 'events'; -import { cloneDeep } from 'lodash'; -import { stateMonitorFactory } from '../state_monitor_factory'; - -describe('stateMonitorFactory', function () { - const noop = () => {}; - const eventTypes = ['save_with_changes', 'reset_with_changes', 'fetch_with_changes']; - - let mockState; - - function setState(mockState, obj, emit = true) { - mockState.toJSON = () => cloneDeep(obj); - if (emit) mockState.emit(eventTypes[0]); - } - - function createMockState(state = {}) { - const mockState = new EventEmitter(); - setState(mockState, state, false); - return mockState; - } - - beforeEach(() => { - mockState = createMockState({}); - }); - - it('should have a create method', function () { - expect(stateMonitorFactory).to.have.property('create'); - expect(stateMonitorFactory.create).to.be.a('function'); - }); - - describe('factory creation', function () { - it('should not call onChange with only the state', function () { - const monitor = stateMonitorFactory.create(mockState); - const changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - }); - - it('should not call onChange with matching defaultState', function () { - const monitor = stateMonitorFactory.create(mockState, {}); - const changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - }); - - it('should call onChange with differing defaultState', function () { - const monitor = stateMonitorFactory.create(mockState, { test: true }); - const changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.calledOnce(changeStub); - }); - }); - - describe('instance', function () { - let monitor; - - beforeEach(() => { - monitor = stateMonitorFactory.create(mockState); - }); - - describe('onChange', function () { - it('should throw if not given a handler function', function () { - const fn = () => monitor.onChange('not a function'); - expect(fn).to.throwException(/must be a function/); - }); - - eventTypes.forEach((eventType) => { - describe(`when ${eventType} is emitted`, function () { - let handlerFn; - - beforeEach(() => { - handlerFn = sinon.stub(); - monitor.onChange(handlerFn); - sinon.assert.notCalled(handlerFn); - }); - - it('should get called', function () { - mockState.emit(eventType); - sinon.assert.calledOnce(handlerFn); - }); - - it('should be given the state status', function () { - mockState.emit(eventType); - const args = handlerFn.firstCall.args; - expect(args[0]).to.be.an('object'); - }); - - it('should be given the event type', function () { - mockState.emit(eventType); - const args = handlerFn.firstCall.args; - expect(args[1]).to.equal(eventType); - }); - - it('should be given the changed keys', function () { - const keys = ['one', 'two', 'three']; - mockState.emit(eventType, keys); - const args = handlerFn.firstCall.args; - expect(args[2]).to.equal(keys); - }); - }); - }); - }); - - describe('ignoreProps', function () { - it('should not set status to dirty when ignored properties change', function () { - let status; - const mockState = createMockState({ messages: { world: 'hello', foo: 'bar' } }); - const monitor = stateMonitorFactory.create(mockState); - const changeStub = sinon.stub(); - monitor.ignoreProps('messages.world'); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - - // update the ignored state prop - setState(mockState, { messages: { world: 'howdy', foo: 'bar' } }); - sinon.assert.calledOnce(changeStub); - status = changeStub.firstCall.args[0]; - expect(status).to.have.property('clean', true); - expect(status).to.have.property('dirty', false); - - // update a prop that is not ignored - setState(mockState, { messages: { world: 'howdy', foo: 'baz' } }); - sinon.assert.calledTwice(changeStub); - status = changeStub.secondCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - }); - }); - - describe('setInitialState', function () { - let changeStub; - - beforeEach(() => { - changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - }); - - it('should throw if no state is provided', function () { - const fn = () => monitor.setInitialState(); - expect(fn).to.throwException(/must be an object/); - }); - - it('should throw if given the wrong type', function () { - const fn = () => monitor.setInitialState([]); - expect(fn).to.throwException(/must be an object/); - }); - - it('should trigger the onChange handler', function () { - monitor.setInitialState({ new: 'state' }); - sinon.assert.calledOnce(changeStub); - }); - - it('should change the status with differing state', function () { - monitor.setInitialState({ new: 'state' }); - sinon.assert.calledOnce(changeStub); - - const status = changeStub.firstCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - }); - - it('should not trigger the onChange handler without state change', function () { - monitor.setInitialState(cloneDeep(mockState.toJSON())); - sinon.assert.notCalled(changeStub); - }); - }); - - describe('status object', function () { - let handlerFn; - - beforeEach(() => { - handlerFn = sinon.stub(); - monitor.onChange(handlerFn); - }); - - it('should be clean by default', function () { - mockState.emit(eventTypes[0]); - const status = handlerFn.firstCall.args[0]; - expect(status).to.have.property('clean', true); - expect(status).to.have.property('dirty', false); - }); - - it('should be dirty when state changes', function () { - setState(mockState, { message: 'i am dirty now' }); - const status = handlerFn.firstCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - }); - - it('should be clean when state is reset', function () { - const defaultState = { message: 'i am the original state' }; - const handlerFn = sinon.stub(); - - let status; - - // initial state and monitor setup - const mockState = createMockState(defaultState); - const monitor = stateMonitorFactory.create(mockState); - monitor.onChange(handlerFn); - sinon.assert.notCalled(handlerFn); - - // change the state and emit an event - setState(mockState, { message: 'i am dirty now' }); - sinon.assert.calledOnce(handlerFn); - status = handlerFn.firstCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - - // reset the state and emit an event - setState(mockState, defaultState); - sinon.assert.calledTwice(handlerFn); - status = handlerFn.secondCall.args[0]; - expect(status).to.have.property('clean', true); - expect(status).to.have.property('dirty', false); - }); - }); - - describe('destroy', function () { - let stateSpy; - - beforeEach(() => { - stateSpy = sinon.spy(mockState, 'off'); - sinon.assert.notCalled(stateSpy); - }); - - it('should remove the listeners', function () { - monitor.onChange(noop); - monitor.destroy(); - sinon.assert.callCount(stateSpy, eventTypes.length); - eventTypes.forEach((eventType) => { - sinon.assert.calledWith(stateSpy, eventType); - }); - }); - - it('should stop the instance from being used any more', function () { - monitor.onChange(noop); - monitor.destroy(); - const fn = () => monitor.onChange(noop); - expect(fn).to.throwException(/has been destroyed/); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/app_state.d.ts b/src/legacy/ui/public/state_management/app_state.d.ts deleted file mode 100644 index ddd0b90710766..0000000000000 --- a/src/legacy/ui/public/state_management/app_state.d.ts +++ /dev/null @@ -1,26 +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 { State } from './state'; - -export class AppState extends State {} - -export type AppStateClass< - TAppState extends AppState = AppState, - TDefaults = { [key: string]: any } -> = new (defaults: TDefaults) => TAppState; diff --git a/src/legacy/ui/public/state_management/app_state.js b/src/legacy/ui/public/state_management/app_state.js deleted file mode 100644 index ec680d163b9da..0000000000000 --- a/src/legacy/ui/public/state_management/app_state.js +++ /dev/null @@ -1,137 +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. - */ - -/** - * @name AppState - * - * @extends State - * - * @description Inherits State, which inherits Events. This class seems to be - * concerned with mapping "props" to PersistedState instances, and surfacing the - * ability to destroy those mappings. - */ - -import { uiModules } from '../modules'; -import { StateProvider } from './state'; -import { PersistedState } from '../../../../plugins/visualizations/public'; -import { createLegacyClass } from '../utils/legacy_class'; - -const urlParam = '_a'; - -export function AppStateProvider(Private, $location) { - const State = Private(StateProvider); - - let persistedStates; - let eventUnsubscribers; - - createLegacyClass(AppState).inherits(State); - function AppState(defaults) { - // Initialize persistedStates. This object maps "prop" names to - // PersistedState instances. These are used to make properties "stateful". - persistedStates = {}; - - // Initialize eventUnsubscribers. These will be called in `destroy`, to - // remove handlers for the 'change' and 'fetch_with_changes' events which - // are dispatched via the rootScope. - eventUnsubscribers = []; - - AppState.Super.call(this, urlParam, defaults); - AppState.getAppState._set(this); - } - - // if the url param is missing, write it back - AppState.prototype._persistAcrossApps = false; - - AppState.prototype.destroy = function () { - AppState.Super.prototype.destroy.call(this); - AppState.getAppState._set(null); - - eventUnsubscribers.forEach((listener) => listener()); - }; - - /** - * @returns PersistedState instance. - */ - AppState.prototype.makeStateful = function (prop) { - if (persistedStates[prop]) return persistedStates[prop]; - const self = this; - - // set up the ui state - persistedStates[prop] = new PersistedState(); - - // update the app state when the stateful instance changes - const updateOnChange = function () { - const replaceState = false; // TODO: debouncing logic - self[prop] = persistedStates[prop].getChanges(); - // Save state to the URL. - self.save(replaceState); - }; - const handlerOnChange = (method) => persistedStates[prop][method]('change', updateOnChange); - handlerOnChange('on'); - eventUnsubscribers.push(() => handlerOnChange('off')); - - // update the stateful object when the app state changes - const persistOnChange = function (changes) { - if (!changes) return; - - if (changes.indexOf(prop) !== -1) { - persistedStates[prop].set(self[prop]); - } - }; - const handlePersist = (method) => this[method]('fetch_with_changes', persistOnChange); - handlePersist('on'); - eventUnsubscribers.push(() => handlePersist('off')); - - // if the thing we're making stateful has an appState value, write to persisted state - if (self[prop]) persistedStates[prop].setSilent(self[prop]); - - return persistedStates[prop]; - }; - - AppState.getAppState = (function () { - let currentAppState; - - function get() { - return currentAppState; - } - - // Checks to see if the appState might already exist, even if it hasn't been newed up - get.previouslyStored = function () { - const search = $location.search(); - return search[urlParam] ? true : false; - }; - - get._set = function (current) { - currentAppState = current; - }; - - return get; - })(); - - return AppState; -} - -uiModules - .get('kibana/global_state') - .factory('AppState', function (Private) { - return Private(AppStateProvider); - }) - .service('getAppState', function (Private) { - return Private(AppStateProvider).getAppState; - }); diff --git a/src/legacy/ui/public/state_management/config_provider.js b/src/legacy/ui/public/state_management/config_provider.js deleted file mode 100644 index 68de449a57a56..0000000000000 --- a/src/legacy/ui/public/state_management/config_provider.js +++ /dev/null @@ -1,48 +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. - */ - -/** - * @name stateManagementConfig - * - * @description Allows apps to configure state management - */ - -import { uiModules } from '../modules'; - -export class StateManagementConfigProvider { - _enabled = true; - - $get(/* inject stuff */) { - return { - enabled: this._enabled, - }; - } - - disable() { - this._enabled = false; - } - - enable() { - this._enabled = true; - } -} - -uiModules - .get('kibana/state_management') - .provider('stateManagementConfig', StateManagementConfigProvider); diff --git a/src/legacy/ui/public/state_management/global_state.d.ts b/src/legacy/ui/public/state_management/global_state.d.ts deleted file mode 100644 index 66a85d88956c7..0000000000000 --- a/src/legacy/ui/public/state_management/global_state.d.ts +++ /dev/null @@ -1,22 +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 { State } from './state'; - -export type GlobalState = State; diff --git a/src/legacy/ui/public/state_management/global_state.js b/src/legacy/ui/public/state_management/global_state.js deleted file mode 100644 index 0e8dfe40d5950..0000000000000 --- a/src/legacy/ui/public/state_management/global_state.js +++ /dev/null @@ -1,42 +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 { StateProvider } from './state'; -import { uiModules } from '../modules'; -import { createLegacyClass } from '../utils/legacy_class'; - -const module = uiModules.get('kibana/global_state'); - -export function GlobalStateProvider(Private) { - const State = Private(StateProvider); - - createLegacyClass(GlobalState).inherits(State); - function GlobalState(defaults) { - GlobalState.Super.call(this, '_g', defaults); - } - - // if the url param is missing, write it back - GlobalState.prototype._persistAcrossApps = true; - - return new GlobalState(); -} - -module.service('globalState', function (Private) { - return Private(GlobalStateProvider); -}); diff --git a/src/legacy/ui/public/state_management/state.d.ts b/src/legacy/ui/public/state_management/state.d.ts deleted file mode 100644 index b31862b681f55..0000000000000 --- a/src/legacy/ui/public/state_management/state.d.ts +++ /dev/null @@ -1,24 +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 State { - [key: string]: any; - translateHashToRison: (stateHashOrRison: string | string[]) => string | string[]; - getQueryParamName: () => string; -} diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js deleted file mode 100644 index d91834adb4a79..0000000000000 --- a/src/legacy/ui/public/state_management/state.js +++ /dev/null @@ -1,359 +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. - */ - -/** - * @name State - * - * @extends Events - * - * @description Persists generic "state" to and reads it from the URL. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import angular from 'angular'; -import rison from 'rison-node'; -import { EventsProvider } from '../events'; -import { fatalError, toastNotifications } from '../notify'; -import './config_provider'; -import { createLegacyClass } from '../utils/legacy_class'; -import { - hashedItemStore, - isStateHash, - createStateHash, - applyDiff, -} from '../../../../plugins/kibana_utils/public'; - -export function StateProvider( - Private, - $rootScope, - $location, - stateManagementConfig, - config, - kbnUrl, - $injector -) { - const Events = Private(EventsProvider); - - const isDummyRoute = () => - $injector.has('$route') && - $injector.get('$route').current && - $injector.get('$route').current.outerAngularWrapperRoute; - - createLegacyClass(State).inherits(Events); - function State(urlParam, defaults, _hashedItemStore = hashedItemStore) { - State.Super.call(this); - - this.setDefaults(defaults); - this._urlParam = urlParam || '_s'; - this._hashedItemStore = _hashedItemStore; - - // When the URL updates we need to fetch the values from the URL - this._cleanUpListeners = [ - // partial route update, no app reload - $rootScope.$on('$routeUpdate', () => { - this.fetch(); - }), - - // beginning of full route update, new app will be initialized before - // $routeChangeSuccess or $routeChangeError - $rootScope.$on('$routeChangeStart', () => { - if (!this._persistAcrossApps) { - this.destroy(); - } - }), - - $rootScope.$on('$routeChangeSuccess', () => { - if (this._persistAcrossApps) { - this.fetch(); - } - }), - ]; - - // Initialize the State with fetch - this.fetch(); - } - - State.prototype._readFromURL = function () { - const search = $location.search(); - const urlVal = search[this._urlParam]; - - if (!urlVal) { - return null; - } - - if (isStateHash(urlVal)) { - return this._parseStateHash(urlVal); - } - - let risonEncoded; - let unableToParse; - try { - risonEncoded = rison.decode(urlVal); - } catch (e) { - unableToParse = true; - } - - if (unableToParse) { - toastNotifications.addDanger( - i18n.translate('common.ui.stateManagement.unableToParseUrlErrorMessage', { - defaultMessage: 'Unable to parse URL', - }) - ); - search[this._urlParam] = this.toQueryParam(this._defaults); - $location.search(search).replace(); - } - - if (!risonEncoded) { - return null; - } - - if (this.isHashingEnabled()) { - // RISON can find its way into the URL any number of ways, including the navbar links or - // shared urls with the entire state embedded. These values need to be translated into - // hashes and replaced in the browser history when state-hashing is enabled - search[this._urlParam] = this.toQueryParam(risonEncoded); - $location.search(search).replace(); - } - - return risonEncoded; - }; - - /** - * Fetches the state from the url - * @returns {void} - */ - State.prototype.fetch = function () { - if (!stateManagementConfig.enabled) { - return; - } - - let stash = this._readFromURL(); - - // nothing to read from the url? save if ordered to persist, but only if it's not on a wrapper route - if (stash === null) { - if (this._persistAcrossApps) { - return this.save(); - } else { - stash = {}; - } - } - - _.defaults(stash, this._defaults); - // apply diff to state from stash, will change state in place via side effect - const diffResults = applyDiff(this, stash); - - if (!isDummyRoute() && diffResults.keys.length) { - this.emit('fetch_with_changes', diffResults.keys); - } - }; - - /** - * Saves the state to the url - * @returns {void} - */ - State.prototype.save = function (replace) { - if (!stateManagementConfig.enabled) { - return; - } - - if (isDummyRoute()) { - return; - } - - let stash = this._readFromURL(); - const state = this.toObject(); - replace = replace || false; - - if (!stash) { - replace = true; - stash = {}; - } - - // apply diff to state from stash, will change state in place via side effect - const diffResults = applyDiff(stash, _.defaults({}, state, this._defaults)); - - if (diffResults.keys.length) { - this.emit('save_with_changes', diffResults.keys); - } - - // persist the state in the URL - const search = $location.search(); - search[this._urlParam] = this.toQueryParam(state); - if (replace) { - $location.search(search).replace(); - } else { - $location.search(search); - } - }; - - /** - * Calls save with a forced replace - * @returns {void} - */ - State.prototype.replace = function () { - if (!stateManagementConfig.enabled) { - return; - } - - this.save(true); - }; - - /** - * Resets the state to the defaults - * - * @returns {void} - */ - State.prototype.reset = function () { - if (!stateManagementConfig.enabled) { - return; - } - - kbnUrl.removeParam(this.getQueryParamName()); - // apply diff to attributes from defaults, this is side effecting so - // it will change the state in place. - const diffResults = applyDiff(this, this._defaults); - if (diffResults.keys.length) { - this.emit('reset_with_changes', diffResults.keys); - } - this.save(); - }; - - /** - * Cleans up the state object - * @returns {void} - */ - State.prototype.destroy = function () { - this.off(); // removes all listeners - - // Removes the $routeUpdate listener - this._cleanUpListeners.forEach((listener) => listener(this)); - }; - - State.prototype.setDefaults = function (defaults) { - this._defaults = defaults || {}; - }; - - /** - * Parse the state hash to it's unserialized value. Hashes are restored - * to their pre-hashed state. - * - * @param {string} stateHash - state hash value from the query string. - * @return {any} - the stored value, or null if hash does not resolve. - */ - State.prototype._parseStateHash = function (stateHash) { - const json = this._hashedItemStore.getItem(stateHash); - if (json === null) { - toastNotifications.addDanger( - i18n.translate('common.ui.stateManagement.unableToRestoreUrlErrorMessage', { - defaultMessage: - 'Unable to completely restore the URL, be sure to use the share functionality.', - }) - ); - } - - return JSON.parse(json); - }; - - /** - * Lookup the value for a hash and return it's value in rison format or just - * return passed argument if it's not recognized as state hash. - * - * @param {string} stateHashOrRison - either state hash value or rison string. - * @return {string} rison - */ - State.prototype.translateHashToRison = function (stateHashOrRison) { - if (isStateHash(stateHashOrRison)) { - return rison.encode(this._parseStateHash(stateHashOrRison)); - } - - return stateHashOrRison; - }; - - State.prototype.isHashingEnabled = function () { - return !!config.get('state:storeInSessionStorage'); - }; - - /** - * Produce the hash version of the state in it's current position - * - * @return {string} - */ - State.prototype.toQueryParam = function (state = this.toObject()) { - if (!this.isHashingEnabled()) { - return rison.encode(state); - } - - // We need to strip out Angular-specific properties. - const json = angular.toJson(state); - const hash = createStateHash(json); - const isItemSet = this._hashedItemStore.setItem(hash, json); - - if (isItemSet) { - return hash; - } - - // If we ran out of space trying to persist the state, notify the user. - const message = i18n.translate( - 'common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage', - { - defaultMessage: - 'Kibana is unable to store history items in your session ' + - `because it is full and there don't seem to be items any items safe ` + - 'to delete.\n\n' + - 'This can usually be fixed by moving to a fresh tab, but could ' + - 'be caused by a larger issue. If you are seeing this message regularly, ' + - 'please file an issue at {gitHubIssuesUrl}.', - values: { gitHubIssuesUrl: 'https://github.com/elastic/kibana/issues' }, - } - ); - fatalError(new Error(message)); - }; - - /** - * Get the query string parameter name where this state writes and reads - * @return {string} - */ - State.prototype.getQueryParamName = function () { - return this._urlParam; - }; - - /** - * Returns an object with each property name and value corresponding to the entries in this collection - * excluding fields started from '$', '_' and all methods - * - * @return {object} - */ - State.prototype.toObject = function () { - return _.omitBy(this, (value, key) => { - return key.charAt(0) === '$' || key.charAt(0) === '_' || _.isFunction(value); - }); - }; - - /** Alias for method 'toObject' - * - * @obsolete Please use 'toObject' method instead - * @return {object} - */ - State.prototype.toJSON = function () { - return this.toObject(); - }; - - return State; -} diff --git a/src/legacy/ui/public/state_management/state_monitor_factory.ts b/src/legacy/ui/public/state_management/state_monitor_factory.ts deleted file mode 100644 index 968ececfe3be5..0000000000000 --- a/src/legacy/ui/public/state_management/state_monitor_factory.ts +++ /dev/null @@ -1,157 +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 { cloneDeep, isEqual, isPlainObject } from 'lodash'; -import { State } from './state'; - -export const stateMonitorFactory = { - create: ( - state: State, - customInitialState: TStateDefault - ) => stateMonitor(state, customInitialState), -}; - -interface StateStatus { - clean: boolean; - dirty: boolean; -} - -export interface StateMonitor { - setInitialState: (innerCustomInitialState: TStateDefault) => void; - ignoreProps: (props: string[] | string) => void; - onChange: (callback: ChangeHandlerFn) => StateMonitor; - destroy: () => void; -} - -type ChangeHandlerFn = (status: StateStatus, type: string | null, keys: string[]) => void; - -function stateMonitor( - state: State, - customInitialState: TStateDefault -): StateMonitor { - let destroyed = false; - let ignoredProps: string[] = []; - let changeHandlers: ChangeHandlerFn[] | undefined = []; - let initialState: TStateDefault; - - setInitialState(customInitialState); - - function setInitialState(innerCustomInitialState: TStateDefault) { - // state.toJSON returns a reference, clone so we can mutate it safely - initialState = cloneDeep(innerCustomInitialState) || cloneDeep(state.toJSON()); - } - - function removeIgnoredProps(innerState: TStateDefault) { - ignoredProps.forEach((path) => { - set(innerState, path, true); - }); - return innerState; - } - - function getStatus(): StateStatus { - // state.toJSON returns a reference, clone so we can mutate it safely - const currentState = removeIgnoredProps(cloneDeep(state.toJSON())); - const isClean = isEqual(currentState, initialState); - - return { - clean: isClean, - dirty: !isClean, - }; - } - - function dispatchChange(type: string | null = null, keys: string[] = []) { - const status = getStatus(); - if (!changeHandlers) { - throw new Error('Change handlers is undefined, this object has been destroyed'); - } - changeHandlers.forEach((changeHandler) => { - changeHandler(status, type, keys); - }); - } - - function dispatchFetch(keys: string[]) { - dispatchChange('fetch_with_changes', keys); - } - - function dispatchSave(keys: string[]) { - dispatchChange('save_with_changes', keys); - } - - function dispatchReset(keys: string[]) { - dispatchChange('reset_with_changes', keys); - } - - return { - setInitialState(innerCustomInitialState: TStateDefault) { - if (!isPlainObject(innerCustomInitialState)) { - throw new TypeError('The default state must be an object'); - } - - // check the current status - const previousStatus = getStatus(); - - // update the initialState and apply ignoredProps - setInitialState(innerCustomInitialState); - removeIgnoredProps(initialState); - - // fire the change handler if the status has changed - if (!isEqual(previousStatus, getStatus())) { - dispatchChange(); - } - }, - - ignoreProps(props: string[] | string) { - ignoredProps = ignoredProps.concat(props); - removeIgnoredProps(initialState); - return this; - }, - - onChange(callback: ChangeHandlerFn) { - if (destroyed || !changeHandlers) { - throw new Error('Monitor has been destroyed'); - } - if (typeof callback !== 'function') { - throw new Error('onChange handler must be a function'); - } - - changeHandlers.push(callback); - - // Listen for state events. - state.on('fetch_with_changes', dispatchFetch); - state.on('save_with_changes', dispatchSave); - state.on('reset_with_changes', dispatchReset); - - // if the state is already dirty, fire the change handler immediately - const status = getStatus(); - if (status.dirty) { - dispatchChange(); - } - - return this; - }, - - destroy() { - destroyed = true; - changeHandlers = undefined; - state.off('fetch_with_changes', dispatchFetch); - state.off('save_with_changes', dispatchSave); - state.off('reset_with_changes', dispatchReset); - }, - }; -} diff --git a/src/legacy/ui/public/system_api/__tests__/system_api.js b/src/legacy/ui/public/system_api/__tests__/system_api.js deleted file mode 100644 index 816024f13f8b2..0000000000000 --- a/src/legacy/ui/public/system_api/__tests__/system_api.js +++ /dev/null @@ -1,68 +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 { - addSystemApiHeader, - isSystemApiRequest, -} from '../../../../../plugins/kibana_legacy/public'; - -describe('system_api', () => { - describe('#addSystemApiHeader', () => { - it('adds the correct system API header', () => { - const headers = { - 'kbn-version': '4.6.0', - }; - const newHeaders = addSystemApiHeader(headers); - - expect(newHeaders).to.have.property('kbn-system-request'); - expect(newHeaders['kbn-system-request']).to.be(true); - - expect(newHeaders).to.have.property('kbn-version'); - expect(newHeaders['kbn-version']).to.be('4.6.0'); - }); - }); - - describe('#isSystemApiRequest', () => { - it('returns true for a system HTTP request', () => { - const mockRequest = { - headers: { - 'kbn-system-request': true, - }, - }; - expect(isSystemApiRequest(mockRequest)).to.be(true); - }); - - it('returns true for a legacy system API HTTP request', () => { - const mockRequest = { - headers: { - 'kbn-system-api': true, - }, - }; - expect(isSystemApiRequest(mockRequest)).to.be(true); - }); - - it('returns false for a non-system API HTTP request', () => { - const mockRequest = { - headers: {}, - }; - expect(isSystemApiRequest(mockRequest)).to.be(false); - }); - }); -}); diff --git a/src/legacy/ui/public/system_api/index.js b/src/legacy/ui/public/system_api/index.js deleted file mode 100644 index 6361c0ea6c69a..0000000000000 --- a/src/legacy/ui/public/system_api/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 { addSystemApiHeader, isSystemApiRequest } from '../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/ui/public/timefilter/index.ts b/src/legacy/ui/public/timefilter/index.ts deleted file mode 100644 index 83795c73112be..0000000000000 --- a/src/legacy/ui/public/timefilter/index.ts +++ /dev/null @@ -1,39 +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 uiRoutes from 'ui/routes'; -import { npStart } from 'ui/new_platform'; - -import { TimefilterContract, TimeHistoryContract } from '../../../../plugins/data/public'; -import { registerTimefilterWithGlobalState } from './setup_router'; - -export { - getTime, - InputTimeRange, - TimeHistoryContract, - TimefilterContract, -} from '../../../../plugins/data/public'; -export type Timefilter = TimefilterContract; -export type TimeHistory = TimeHistoryContract; -export const timeHistory = npStart.plugins.data.query.timefilter.history; -export const timefilter = npStart.plugins.data.query.timefilter.timefilter; - -uiRoutes.addSetupWork((globalState, $rootScope) => { - return registerTimefilterWithGlobalState(timefilter, globalState, $rootScope); -}); diff --git a/src/legacy/ui/public/timefilter/setup_router.test.js b/src/legacy/ui/public/timefilter/setup_router.test.js deleted file mode 100644 index 2ae9a053ae2db..0000000000000 --- a/src/legacy/ui/public/timefilter/setup_router.test.js +++ /dev/null @@ -1,54 +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 { registerTimefilterWithGlobalState } from './setup_router'; - -jest.mock('../../../../plugins/kibana_legacy/public', () => ({ - subscribeWithScope: jest.fn(), -})); - -describe('registerTimefilterWithGlobalState()', () => { - it('should always use iso8601 strings', async () => { - const setTime = jest.fn(); - const timefilter = { - setTime, - setRefreshInterval: jest.fn(), - getRefreshIntervalUpdate$: jest.fn(), - getTimeUpdate$: jest.fn(), - }; - - const globalState = { - time: { - from: '2017-09-07T20:12:04.011Z', - to: '2017-09-07T20:18:55.733Z', - }, - on: (eventName, callback) => { - callback(); - }, - }; - - const rootScope = { - $on: jest.fn(), - }; - - registerTimefilterWithGlobalState(timefilter, globalState, rootScope); - - expect(setTime.mock.calls.length).toBe(2); - expect(setTime.mock.calls[1][0]).toEqual(globalState.time); - }); -}); diff --git a/src/legacy/ui/public/timefilter/setup_router.ts b/src/legacy/ui/public/timefilter/setup_router.ts deleted file mode 100644 index 7c25c6aa3166e..0000000000000 --- a/src/legacy/ui/public/timefilter/setup_router.ts +++ /dev/null @@ -1,118 +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 _ from 'lodash'; -import { IScope } from 'angular'; -import moment from 'moment'; -import chrome from 'ui/chrome'; -import { Subscription } from 'rxjs'; -import { fatalError } from 'ui/notify/fatal_error'; -import { subscribeWithScope } from '../../../../plugins/kibana_legacy/public'; -import { - RefreshInterval, - TimeRange, - TimefilterContract, - UI_SETTINGS, -} from '../../../../plugins/data/public'; - -// TODO -// remove everything underneath once globalState is no longer an angular service -// and listener can be registered without angular. -function convertISO8601(stringTime: string): string { - const obj = moment(stringTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); - return obj.isValid() ? obj.toISOString() : stringTime; -} - -export function getTimefilterConfig() { - const settings = chrome.getUiSettingsClient(); - return { - timeDefaults: settings.get('timepicker:timeDefaults'), - refreshIntervalDefaults: settings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), - }; -} - -export const registerTimefilterWithGlobalStateFactory = ( - timefilter: TimefilterContract, - globalState: any, - $rootScope: IScope -) => { - // settings have to be re-fetched here, to make sure that settings changed by overrideLocalDefault are taken into account. - const config = getTimefilterConfig(); - timefilter.setTime(_.defaults(globalState.time || {}, config.timeDefaults)); - timefilter.setRefreshInterval( - _.defaults(globalState.refreshInterval || {}, config.refreshIntervalDefaults) - ); - - globalState.on('fetch_with_changes', () => { - // clone and default to {} in one - const newTime: TimeRange = _.defaults({}, globalState.time, config.timeDefaults); - const newRefreshInterval: RefreshInterval = _.defaults( - {}, - globalState.refreshInterval, - config.refreshIntervalDefaults - ); - - if (newTime) { - if (newTime.to) newTime.to = convertISO8601(newTime.to); - if (newTime.from) newTime.from = convertISO8601(newTime.from); - } - - timefilter.setTime(newTime); - timefilter.setRefreshInterval(newRefreshInterval); - }); - - const updateGlobalStateWithTime = () => { - globalState.time = timefilter.getTime(); - globalState.refreshInterval = timefilter.getRefreshInterval(); - globalState.save(); - }; - - const subscriptions = new Subscription(); - subscriptions.add( - subscribeWithScope( - $rootScope, - timefilter.getRefreshIntervalUpdate$(), - { - next: updateGlobalStateWithTime, - }, - fatalError - ) - ); - - subscriptions.add( - subscribeWithScope( - $rootScope, - timefilter.getTimeUpdate$(), - { - next: updateGlobalStateWithTime, - }, - fatalError - ) - ); - - $rootScope.$on('$destroy', () => { - subscriptions.unsubscribe(); - }); -}; - -// Currently some parts of Kibana (index patterns, timefilter) rely on addSetupWork in the uiRouter -// and require it to be executed to properly function. -// This function is exposed for applications that do not use uiRoutes like APM -// Kibana issue https://github.com/elastic/kibana/issues/19110 tracks the removal of this dependency on uiRouter -export const registerTimefilterWithGlobalState = _.once(registerTimefilterWithGlobalStateFactory); diff --git a/src/legacy/ui/public/url/__tests__/extract_app_path_and_id.js b/src/legacy/ui/public/url/__tests__/extract_app_path_and_id.js deleted file mode 100644 index 965e8f4bc9f38..0000000000000 --- a/src/legacy/ui/public/url/__tests__/extract_app_path_and_id.js +++ /dev/null @@ -1,122 +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 { extractAppPathAndId } from '../extract_app_path_and_id'; - -describe('extractAppPathAndId', function () { - describe('from an absolute url with a base path', function () { - describe('with a base path', function () { - const basePath = '/gza'; - const absoluteUrl = 'http://www.test.com:5601/gza/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(absoluteUrl, basePath).appPath).to.be( - 'appPathIsHere?query=here' - ); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(absoluteUrl, basePath).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('http://www.test.com:5601/gza/noapppath'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('without a base path', function () { - const absoluteUrl = 'http://www.test.com:5601/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(absoluteUrl).appPath).to.be('appPathIsHere?query=here'); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(absoluteUrl).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('http://www.test.com:5601/noapppath'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('when appPath is empty', function () { - const absoluteUrl = 'http://www.test.com:5601/app/appId'; - it('extracts app id', function () { - expect(extractAppPathAndId(absoluteUrl).appId).to.be('appId'); - }); - it('extracts empty appPath', function () { - expect(extractAppPathAndId(absoluteUrl).appPath).to.be(''); - }); - }); - }); - - describe('from a root relative url', function () { - describe('with a base path', function () { - const basePath = '/gza'; - const rootRelativePath = '/gza/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(rootRelativePath, basePath).appPath).to.be( - 'appPathIsHere?query=here' - ); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(rootRelativePath, basePath).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('/gza/notformattedright'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('without a base path', function () { - const rootRelativePath = '/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(rootRelativePath).appPath).to.be('appPathIsHere?query=here'); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(rootRelativePath).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('/notformattedright'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('when appPath is empty', function () { - const rootRelativePath = '/app/appId'; - it('extracts app id', function () { - expect(extractAppPathAndId(rootRelativePath).appId).to.be('appId'); - }); - it('extracts empty appPath', function () { - expect(extractAppPathAndId(rootRelativePath).appPath).to.be(''); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/url/__tests__/kibana_parsed_url.js b/src/legacy/ui/public/url/__tests__/kibana_parsed_url.js deleted file mode 100644 index 6ea199c3d22cc..0000000000000 --- a/src/legacy/ui/public/url/__tests__/kibana_parsed_url.js +++ /dev/null @@ -1,217 +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 { KibanaParsedUrl } from '../kibana_parsed_url'; - -describe('KibanaParsedUrl', function () { - it('getHashedAppPath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/hi', - appId: 'bye', - appPath: 'visualize?hi=there&bye', - }); - expect(kibanaParsedUrl.getHashedAppPath()).to.be('#visualize?hi=there&bye'); - }); - - it('getAppRootPath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/hi', - appId: 'appId', - appPath: 'dashboard?edit=123', - }); - expect(kibanaParsedUrl.getAppRootPath()).to.be('/app/appId#dashboard?edit=123'); - }); - - describe('when basePath is specified', function () { - it('getRootRelativePath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize?hi=there&bye', - }); - expect(kibanaParsedUrl.getRootRelativePath()).to.be('/base/app/appId#visualize?hi=there&bye'); - }); - - describe('getAbsolutePath', function () { - const protocol = 'http'; - const hostname = 'www.test.com'; - const port = '5601'; - - it('returns the absolute url when there is a port', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize?hi=there&bye', - hostname, - protocol, - port, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com:5601/base/app/appId#visualize?hi=there&bye' - ); - }); - - it('returns the absolute url when there are no query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com/base/app/appId#visualize' - ); - }); - - it('returns the absolute url when the are query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize?hi=bye&tata', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com/base/app/appId#visualize?hi=bye&tata' - ); - }); - }); - }); - - describe('when basePath is not specified', function () { - it('getRootRelativePath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize?hi=there&bye', - }); - expect(kibanaParsedUrl.getRootRelativePath()).to.be('/app/appId#visualize?hi=there&bye'); - }); - - describe('getAbsolutePath', function () { - const protocol = 'http'; - const hostname = 'www.test.com'; - const port = '5601'; - - it('returns the absolute url when there is a port', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize?hi=there&bye', - hostname, - protocol, - port, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com:5601/app/appId#visualize?hi=there&bye' - ); - }); - - it('returns the absolute url when there are no query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be('http://www.test.com/app/appId#visualize'); - }); - - it('returns the absolute url when there are query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize?hi=bye&tata', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com/app/appId#visualize?hi=bye&tata' - ); - }); - }); - }); - - describe('getGlobalState', function () { - const basePath = '/xyz'; - const appId = 'myApp'; - - it('returns an empty string when the KibanaParsedUrl is in an invalid state', function () { - const url = new KibanaParsedUrl({ basePath }); - expect(url.getGlobalState()).to.be(''); - }); - - it('returns an empty string when there is no global state', function () { - const url = new KibanaParsedUrl({ basePath, appId, appPath: '/hi?notg=something' }); - expect(url.getGlobalState()).to.be(''); - }); - - it('returns the global state when it is the last parameter', function () { - const url = new KibanaParsedUrl({ - basePath, - appId, - appPath: '/hi?notg=something&_g=(thisismyglobalstate)', - }); - expect(url.getGlobalState()).to.be('(thisismyglobalstate)'); - }); - - it('returns the global state when it is the first parameter', function () { - const url = new KibanaParsedUrl({ - basePath, - appId, - appPath: '/hi?_g=(thisismyglobalstate)&hi=bye', - }); - expect(url.getGlobalState()).to.be('(thisismyglobalstate)'); - }); - }); - - describe('setGlobalState', function () { - const basePath = '/xyz'; - const appId = 'myApp'; - - it('does nothing when KibanaParsedUrl is in an invalid state', function () { - const url = new KibanaParsedUrl({ basePath }); - url.setGlobalState('newglobalstate'); - expect(url.getGlobalState()).to.be(''); - }); - - it('clears the global state when setting it to an empty string', function () { - const url = new KibanaParsedUrl({ basePath, appId, appPath: '/hi?_g=globalstate' }); - url.setGlobalState(''); - expect(url.getGlobalState()).to.be(''); - }); - - it('updates the global state when a string is passed in', function () { - const url = new KibanaParsedUrl({ - basePath, - appId, - appPath: '/hi?notg=something&_g=oldstate', - }); - url.setGlobalState('newstate'); - expect(url.getGlobalState()).to.be('newstate'); - }); - - it('adds the global state parameters if it did not exist before', function () { - const url = new KibanaParsedUrl({ basePath, appId, appPath: '/hi' }); - url.setGlobalState('newstate'); - expect(url.getGlobalState()).to.be('newstate'); - expect(url.appPath).to.be('/hi?_g=newstate'); - }); - }); -}); diff --git a/src/legacy/ui/public/url/__tests__/prepend_path.js b/src/legacy/ui/public/url/__tests__/prepend_path.js deleted file mode 100644 index 36991b77553e4..0000000000000 --- a/src/legacy/ui/public/url/__tests__/prepend_path.js +++ /dev/null @@ -1,80 +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 { prependPath } from '../prepend_path'; - -describe('url prependPath', function () { - describe('returns the relative path unchanged', function () { - it('if it is null', function () { - expect(prependPath(null, 'kittens')).to.be(null); - }); - - it('if it is undefined', function () { - expect(prependPath(undefined, 'kittens')).to.be(undefined); - }); - - it('if it is an absolute url', function () { - expect(prependPath('http://www.hithere.com/howareyou', 'welcome')).to.be( - 'http://www.hithere.com/howareyou' - ); - }); - - it('if it does not start with a /', function () { - expect(prependPath('are/so/cool', 'cats')).to.be('are/so/cool'); - }); - - it('when new path is empty', function () { - expect(prependPath('/are/so/cool', '')).to.be('/are/so/cool'); - }); - - it('when it is only a slash and new path is empty', function () { - expect(prependPath('/', '')).to.be('/'); - }); - }); - - describe('returns an updated relative path', function () { - it('when it starts with a slash', function () { - expect(prependPath('/are/so/cool', 'dinosaurs')).to.be('dinosaurs/are/so/cool'); - }); - - it('when new path starts with a slash', function () { - expect(prependPath('/are/so/cool', '/fish')).to.be('/fish/are/so/cool'); - }); - - it('with two slashes if new path is a slash', function () { - expect(prependPath('/are/so/cool', '/')).to.be('//are/so/cool'); - }); - - it('when there is a slash on the end', function () { - expect(prependPath('/are/delicious/', 'lollipops')).to.be('lollipops/are/delicious/'); - }); - - it('when pathname that ends with a file', function () { - expect(prependPath('/are/delicious/index.html', 'donuts')).to.be( - 'donuts/are/delicious/index.html' - ); - }); - - it('when it is only a slash', function () { - expect(prependPath('/', 'kittens')).to.be('kittens/'); - }); - }); -}); diff --git a/src/legacy/ui/public/url/__tests__/url.js b/src/legacy/ui/public/url/__tests__/url.js deleted file mode 100644 index 8b173482e1bb4..0000000000000 --- a/src/legacy/ui/public/url/__tests__/url.js +++ /dev/null @@ -1,562 +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 ngMock from 'ng_mock'; -import faker from 'faker'; -import _ from 'lodash'; -import { AppStateProvider } from '../../state_management/app_state'; -import '..'; - -// global vars, injected and mocked in init() -let kbnUrl; -let $route; -let $location; -let $rootScope; -let appState; - -class StubAppState { - constructor() { - this.getQueryParamName = () => '_a'; - this.toQueryParam = () => 'stateQueryParam'; - this.destroy = sinon.stub(); - } -} - -function init() { - ngMock.module('kibana/url', 'kibana', function ($provide, PrivateProvider) { - $provide.service('$route', function () { - return { - reload: _.noop, - }; - }); - - appState = new StubAppState(); - PrivateProvider.swap(AppStateProvider, ($decorate) => { - const AppState = $decorate(); - AppState.getAppState = () => appState; - return AppState; - }); - }); - - ngMock.inject(function ($injector) { - $route = $injector.get('$route'); - $location = $injector.get('$location'); - $rootScope = $injector.get('$rootScope'); - kbnUrl = $injector.get('kbnUrl'); - }); -} - -describe('kbnUrl', function () { - beforeEach(function () { - init(); - }); - - describe('forcing reload', function () { - it('schedules a listener for $locationChangeSuccess on the $rootScope', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - - sinon.stub($rootScope, '$on'); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - sinon.assert.calledOnce(appState.destroy); - expect($rootScope.$on.callCount).to.be(1); - expect($rootScope.$on.firstCall.args[0]).to.be('$locationChangeSuccess'); - }); - - it('handler unbinds the listener and calls reload', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - - const unbind = sinon.stub(); - sinon.stub($rootScope, '$on').returns(unbind); - $route.reload = sinon.stub(); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - - const handler = $rootScope.$on.firstCall.args[1]; - handler(); - expect(unbind.callCount).to.be(1); - expect($route.reload.callCount).to.be(1); - }); - - it('reloads requested before the first are ignored', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - $route.reload = sinon.stub(); - - sinon.stub($rootScope, '$on').returns(sinon.stub()); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - - // don't call the first handler - - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - }); - - it('one reload can happen once the first has completed', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - $route.reload = sinon.stub(); - - sinon.stub($rootScope, '$on').returns(sinon.stub()); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - - // call the first handler - $rootScope.$on.firstCall.args[1](); - expect($route.reload.callCount).to.be(1); - - expect($rootScope.$on.callCount).to.be(1); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(2); - }); - }); - - describe('remove', function () { - it('removes a parameter with a value from the url', function () { - $location.url('/myurl?exist&WithAParamToRemove=2&anothershouldexist=5'); - kbnUrl.removeParam('WithAParamToRemove'); - expect($location.url()).to.be('/myurl?exist&anothershouldexist=5'); - }); - - it('removes a parameter with no value from the url', function () { - $location.url('/myurl?removeme&hi=5'); - kbnUrl.removeParam('removeme'); - expect($location.url()).to.be('/myurl?hi=5'); - }); - - it('is noop if the given parameter doesn\t exist in the url', function () { - $location.url('/myurl?hi&bye'); - kbnUrl.removeParam('noexist'); - expect($location.url()).to.be('/myurl?hi&bye'); - }); - - it('is noop if given empty string param', function () { - $location.url('/myurl?hi&bye'); - kbnUrl.removeParam(''); - expect($location.url()).to.be('/myurl?hi&bye'); - }); - }); - - describe('change', function () { - it('should set $location.url', function () { - sinon.stub($location, 'url'); - - expect($location.url.callCount).to.be(0); - kbnUrl.change('/some-url'); - expect($location.url.callCount).to.be(1); - }); - - it('should uri encode replaced params', function () { - const url = '/some/path/'; - const params = { replace: faker.Lorem.words(3).join(' ') }; - const check = encodeURIComponent(params.replace); - sinon.stub($location, 'url'); - - kbnUrl.change(url + '{{replace}}', params); - - expect($location.url.firstCall.args[0]).to.be(url + check); - }); - - it('should parse angular expression in substitutions and uri encode the results', function () { - // build url by piecing together these parts - const urlParts = ['/', '/', '?', '&', '#']; - // make sure it can parse templates with weird spacing - const wrappers = [ - ['{{', '}}'], - ['{{ ', ' }}'], - ['{{', ' }}'], - ['{{ ', '}}'], - ['{{ ', ' }}'], - ]; - // make sure filters are evaluated via angular expressions - const objIndex = 4; // used to case one replace as an object - const filters = ['', 'uppercase', '', 'uppercase', '']; - - // the words (template keys) used must all be unique - const words = _.uniq(faker.Lorem.words(10)) - .slice(0, urlParts.length) - .map(function (word, i) { - if (filters[i].length) { - return word + '|' + filters[i]; - } - return word; - }); - - const replacements = faker.Lorem.words(urlParts.length).map(function (word, i) { - // make selected replacement into an object - if (i === objIndex) { - return { replace: word }; - } - - return word; - }); - - // build the url and test url - let url = ''; - let testUrl = ''; - urlParts.forEach(function (part, i) { - url += part + wrappers[i][0] + words[i] + wrappers[i][1]; - const locals = {}; - locals[words[i].split('|')[0]] = replacements[i]; - testUrl += part + encodeURIComponent($rootScope.$eval(words[i], locals)); - }); - - // create the locals replacement object - const params = {}; - replacements.forEach(function (replacement, i) { - const word = words[i].split('|')[0]; - params[word] = replacement; - }); - - sinon.stub($location, 'url'); - - kbnUrl.change(url, params); - - expect($location.url.firstCall.args[0]).to.not.be(url); - expect($location.url.firstCall.args[0]).to.be(testUrl); - }); - - it('should handle dot notation', function () { - const url = '/some/thing/{{that.is.substituted}}'; - - kbnUrl.change(url, { - that: { - is: { - substituted: 'test', - }, - }, - }); - - expect($location.url()).to.be('/some/thing/test'); - }); - - it('should throw when params are missing', function () { - const url = '/{{replace_me}}'; - const params = {}; - - try { - kbnUrl.change(url, params); - throw new Error('this should not run'); - } catch (err) { - expect(err).to.be.an(Error); - expect(err.message).to.match(/replace_me/); - } - }); - - it('should throw when filtered params are missing', function () { - const url = '/{{replace_me|number}}'; - const params = {}; - - try { - kbnUrl.change(url, params); - throw new Error('this should not run'); - } catch (err) { - expect(err).to.be.an(Error); - expect(err.message).to.match(/replace_me\|number/); - } - }); - - it('should change the entire url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.change(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({}); - expect($location.hash()).to.be(''); - }); - - it('should allow setting app state on the target url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.change(newPath, null, new StubAppState()); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({ _a: 'stateQueryParam' }); - expect($location.hash()).to.be(''); - }); - }); - - describe('changePath', function () { - it('should change just the path', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.changePath(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - }); - }); - - describe('redirect', function () { - it('should change the entire url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.redirect(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({}); - expect($location.hash()).to.be(''); - }); - - it('should allow setting app state on the target url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.redirect(newPath, null, new StubAppState()); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({ _a: 'stateQueryParam' }); - expect($location.hash()).to.be(''); - }); - - it('should replace the current history entry', function () { - sinon.stub($location, 'replace'); - $location.url('/some/path'); - - expect($location.replace.callCount).to.be(0); - kbnUrl.redirect('/new/path/'); - expect($location.replace.callCount).to.be(1); - }); - - it('should call replace on $location', function () { - sinon.stub(kbnUrl, '_shouldForceReload').returns(false); - sinon.stub($location, 'replace'); - - expect($location.replace.callCount).to.be(0); - kbnUrl.redirect('/poop'); - expect($location.replace.callCount).to.be(1); - }); - }); - - describe('redirectPath', function () { - it('should only change the path', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.redirectPath(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - }); - - it('should call replace on $location', function () { - sinon.stub(kbnUrl, '_shouldForceReload').returns(false); - sinon.stub($location, 'replace'); - - expect($location.replace.callCount).to.be(0); - kbnUrl.redirectPath('/poop'); - expect($location.replace.callCount).to.be(1); - }); - }); - - describe('_shouldForceReload', function () { - let next; - let prev; - - beforeEach(function () { - $route.current = { - $$route: { - regexp: /^\/is-current-route\/(\d+)/, - reloadOnSearch: true, - }, - }; - - prev = { path: '/is-current-route/1', search: {} }; - next = { path: '/is-current-route/1', search: {} }; - }); - - it("returns false if the passed url doesn't match the current route", function () { - next.path = '/not current'; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - - describe('if the passed url does match the route', function () { - describe('and the route reloads on search', function () { - describe('and the path is the same', function () { - describe('and the search params are the same', function () { - it('returns true', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(true); - }); - }); - describe('but the search params are different', function () { - it('returns false', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - }); - - describe('and the path is different', function () { - beforeEach(function () { - next.path = '/not-same'; - }); - - describe('and the search params are the same', function () { - it('returns false', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - describe('but the search params are different', function () { - it('returns false', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - }); - }); - - describe('but the route does not reload on search', function () { - beforeEach(function () { - $route.current.$$route.reloadOnSearch = false; - }); - - describe('and the path is the same', function () { - describe('and the search params are the same', function () { - it('returns true', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(true); - }); - }); - describe('but the search params are different', function () { - it('returns true', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(true); - }); - }); - }); - - describe('and the path is different', function () { - beforeEach(function () { - next.path = '/not-same'; - }); - - describe('and the search params are the same', function () { - it('returns false', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - describe('but the search params are different', function () { - it('returns false', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - }); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/url/absolute_to_parsed_url.ts b/src/legacy/ui/public/url/absolute_to_parsed_url.ts deleted file mode 100644 index 30f493c25776c..0000000000000 --- a/src/legacy/ui/public/url/absolute_to_parsed_url.ts +++ /dev/null @@ -1,44 +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 { parse } from 'url'; - -import { extractAppPathAndId } from './extract_app_path_and_id'; -import { KibanaParsedUrl } from './kibana_parsed_url'; - -/** - * - * @param absoluteUrl - an absolute url, e.g. https://localhost:5601/gra/app/visualize#/edit/viz_id?hi=bye - * @param basePath - An optional base path for kibana. If supplied, should start with a "/". - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id the basePath is - * "/gra". - * @return {KibanaParsedUrl} - */ -export function absoluteToParsedUrl(absoluteUrl: string, basePath = '') { - const { appPath, appId } = extractAppPathAndId(absoluteUrl, basePath); - const { hostname, port, protocol } = parse(absoluteUrl); - return new KibanaParsedUrl({ - basePath, - appId: appId!, - appPath, - hostname, - port, - protocol, - }); -} diff --git a/src/legacy/ui/public/url/extract_app_path_and_id.ts b/src/legacy/ui/public/url/extract_app_path_and_id.ts deleted file mode 100644 index 44bba272e0873..0000000000000 --- a/src/legacy/ui/public/url/extract_app_path_and_id.ts +++ /dev/null @@ -1,41 +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 { parse } from 'url'; - -/** - * If the url is determined to contain an appId and appPath, it returns those portions. If it is not in the right - * format and an appId and appPath can't be extracted, it returns an empty object. - * @param {string} url - a relative or absolute url which contains an appPath, an appId, and optionally, a basePath. - * @param {string} basePath - optional base path, if given should start with "/". - */ -export function extractAppPathAndId(url: string, basePath = '') { - const parsedUrl = parse(url); - if (!parsedUrl.path) { - return {}; - } - const pathWithoutBase = parsedUrl.path.slice(basePath.length); - - if (!pathWithoutBase.startsWith('/app/')) { - return {}; - } - - const appPath = parsedUrl.hash && parsedUrl.hash.length > 0 ? parsedUrl.hash.slice(1) : ''; - return { appId: pathWithoutBase.slice('/app/'.length), appPath }; -} diff --git a/src/legacy/ui/public/url/index.js b/src/legacy/ui/public/url/index.js deleted file mode 100644 index 8ef267de2890c..0000000000000 --- a/src/legacy/ui/public/url/index.js +++ /dev/null @@ -1,23 +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 { KbnUrlProvider } from './url'; -export { RedirectWhenMissingProvider } from './redirect_when_missing'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { modifyUrl } from '../../../../core/utils'; diff --git a/src/legacy/ui/public/url/kbn_url.ts b/src/legacy/ui/public/url/kbn_url.ts deleted file mode 100644 index 42b6a8f19f9a9..0000000000000 --- a/src/legacy/ui/public/url/kbn_url.ts +++ /dev/null @@ -1,23 +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 interface KbnUrl { - change: (url: string) => void; - removeParam: (param: string) => void; -} diff --git a/src/legacy/ui/public/url/kibana_parsed_url.ts b/src/legacy/ui/public/url/kibana_parsed_url.ts deleted file mode 100644 index 1c60e8729e0ff..0000000000000 --- a/src/legacy/ui/public/url/kibana_parsed_url.ts +++ /dev/null @@ -1,148 +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 { parse } from 'url'; - -import { modifyUrl } from '../../../../core/public'; -import { prependPath } from './prepend_path'; - -interface Options { - /** - * An optional base path for kibana. If supplied, should start with a "/". - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id the - * basePath is "/gra" - */ - basePath?: string; - - /** - * The app id. - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id the app id is "kibana". - */ - appId: string; - - /** - * The path for a page in the the app. Should start with a "/". Don't include the hash sign. Can - * include all query parameters. - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id?g=state the appPath is - * "/edit/viz_id?g=state" - */ - appPath?: string; - - /** - * Optional hostname. Uses current window location's hostname if hostname, port, - * and protocol are undefined. - */ - hostname?: string; - - /** - * Optional port. Uses current window location's port if hostname, port, - * and protocol are undefined. - */ - port?: string; - - /** - * Optional protocol. Uses current window location's protocol if hostname, port, - * and protocol are undefined. - */ - protocol?: string; -} - -/** - * Represents the pieces that make up a url in Kibana, offering some helpful functionality - * for translating those pieces into absolute or relative urls. A Kibana url with a basePath - * looks like this: http://localhost:5601/basePath/app/appId#/an/appPath?with=query¶ms - * - * - basePath is "/basePath" - * - appId is "appId" - * - appPath is "/an/appPath?with=query¶ms" - * - * Almost all urls in Kibana should have this structure, including the "/app" portion in front of the appId - * (one exception is the login link). - */ -export class KibanaParsedUrl { - public appId: string; - public appPath: string; - public basePath: string; - public hostname?: string; - public protocol?: string; - public port?: string; - - constructor(options: Options) { - const { appId, basePath = '', appPath = '', hostname, protocol, port } = options; - - // We'll use window defaults - const hostOrProtocolSpecified = hostname || protocol || port; - - this.basePath = basePath; - this.appId = appId; - this.appPath = appPath; - this.hostname = hostOrProtocolSpecified ? hostname : window.location.hostname; - this.port = hostOrProtocolSpecified ? port : window.location.port; - this.protocol = hostOrProtocolSpecified ? protocol : window.location.protocol; - } - - public getGlobalState() { - if (!this.appPath) { - return ''; - } - const parsedUrl = parse(this.appPath, true); - const query = parsedUrl.query || {}; - return query._g || ''; - } - - public setGlobalState(newGlobalState: string | string[]) { - if (!this.appPath) { - return; - } - - this.appPath = modifyUrl(this.appPath, (parsed) => { - parsed.query._g = newGlobalState; - }); - } - - public addQueryParameter(name: string, val: string) { - this.appPath = modifyUrl(this.appPath, (parsed) => { - parsed.query[name] = val; - }); - } - - public getHashedAppPath() { - return `#${this.appPath}`; - } - - public getAppBasePath() { - return `/${this.appId}`; - } - - public getAppRootPath() { - return `/app${this.getAppBasePath()}${this.getHashedAppPath()}`; - } - - public getRootRelativePath() { - return prependPath(this.getAppRootPath(), this.basePath); - } - - public getAbsoluteUrl() { - return modifyUrl(this.getRootRelativePath(), (parsed) => { - parsed.protocol = this.protocol; - parsed.port = this.port; - parsed.hostname = this.hostname; - }); - } -} diff --git a/src/legacy/ui/public/url/prepend_path.ts b/src/legacy/ui/public/url/prepend_path.ts deleted file mode 100644 index b8a77d5c23bee..0000000000000 --- a/src/legacy/ui/public/url/prepend_path.ts +++ /dev/null @@ -1,50 +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 { isString } from 'lodash'; -import { format, parse } from 'url'; - -/** - * - * @param {string} relativePath - a relative path that must start with a "/". - * @param {string} newPath - the new path to prefix. ex: 'xyz' - * @return {string} the url with the basePath prepended. ex. '/xyz/app/kibana#/management'. If - * the relative path isn't in the right format (e.g. doesn't start with a "/") the relativePath is returned - * unchanged. - */ -export function prependPath(relativePath: string, newPath = '') { - if (!relativePath || !isString(relativePath)) { - return relativePath; - } - - const parsed = parse(relativePath, true, true); - if (!parsed.host && parsed.pathname) { - if (parsed.pathname[0] === '/') { - parsed.pathname = newPath + parsed.pathname; - } - } - - return format({ - protocol: parsed.protocol, - host: parsed.host, - pathname: parsed.pathname, - query: parsed.query, - hash: parsed.hash, - }); -} diff --git a/src/legacy/ui/public/url/redirect_when_missing.js b/src/legacy/ui/public/url/redirect_when_missing.js deleted file mode 100644 index 85c90a14d9fd7..0000000000000 --- a/src/legacy/ui/public/url/redirect_when_missing.js +++ /dev/null @@ -1,71 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { MarkdownSimple } from '../../../../plugins/kibana_react/public'; -import { toastNotifications } from 'ui/notify'; -import { SavedObjectNotFound } from '../../../../plugins/kibana_utils/public'; -import { uiModules } from '../modules'; - -uiModules.get('kibana/url').service('redirectWhenMissing', function (Private) { - return Private(RedirectWhenMissingProvider); -}); - -export function RedirectWhenMissingProvider(kbnUrl, Promise) { - /** - * Creates an error handler that will redirect to a url when a SavedObjectNotFound - * error is thrown - * - * @param {string|object} mapping - a mapping of url's to redirect to based on the saved object that - * couldn't be found, or just a string that will be used for all types - * @return {function} - the handler to pass to .catch() - */ - return function (mapping) { - if (typeof mapping === 'string') { - mapping = { '*': mapping }; - } - - return function (error) { - // if this error is not "404", rethrow - const savedObjectNotFound = error instanceof SavedObjectNotFound; - const unknownVisType = error.message.indexOf('Invalid type') === 0; - if (unknownVisType) { - error.savedObjectType = 'visualization'; - } else if (!savedObjectNotFound) { - throw error; - } - - let url = mapping[error.savedObjectType] || mapping['*']; - if (!url) url = '/'; - - url += (url.indexOf('?') >= 0 ? '&' : '?') + `notFound=${error.savedObjectType}`; - - toastNotifications.addWarning({ - title: i18n.translate('common.ui.url.savedObjectIsMissingNotificationMessage', { - defaultMessage: 'Saved object is missing', - }), - text: {error.message}, - }); - - kbnUrl.redirect(url); - return Promise.halt(); - }; - }; -} diff --git a/src/legacy/ui/public/url/relative_to_absolute.ts b/src/legacy/ui/public/url/relative_to_absolute.ts deleted file mode 100644 index 7d0737d145a1b..0000000000000 --- a/src/legacy/ui/public/url/relative_to_absolute.ts +++ /dev/null @@ -1,34 +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. - */ - -/** - * - * @param {string} url - a relative or root relative url. If a relative path is given then the - * absolute url returned will depend on the current page where this function is called from. For example - * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get - * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that - * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". - * @return {string} the relative url transformed into an absolute url - */ -export function relativeToAbsolute(url: string) { - // convert all link urls to absolute urls - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} diff --git a/src/legacy/ui/public/url/url.js b/src/legacy/ui/public/url/url.js deleted file mode 100644 index fb243b02e05c7..0000000000000 --- a/src/legacy/ui/public/url/url.js +++ /dev/null @@ -1,247 +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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { uiModules } from '../modules'; -import { AppStateProvider } from '../state_management/app_state'; - -uiModules.get('kibana/url').service('kbnUrl', function (Private, $injector) { - //config is not directly used but registers global event listeners to kbnUrl to function - $injector.get('config'); - return Private(KbnUrlProvider); -}); - -export function KbnUrlProvider($injector, $location, $rootScope, $parse, Private) { - /** - * the `kbnUrl` service was created to smooth over some of the - * inconsistent behavior that occurs when modifying the url via - * the `$location` api. In general it is recommended that you use - * the `kbnUrl` service any time you want to modify the url. - * - * "features" that `kbnUrl` does it's best to guarantee, which - * are not guaranteed with the `$location` service: - * - calling `kbnUrl.change()` with a url that resolves to the current - * route will force a full transition (rather than just updating the - * properties of the $route object) - * - * Additional features of `kbnUrl` - * - parameterized urls - * - easily include an app state with the url - * - * @type {KbnUrl} - */ - const self = this; - - /** - * Navigate to a url - * - * @param {String} url - the new url, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the url template - * @return {undefined} - */ - self.change = function (url, paramObj, appState) { - self._changeLocation('url', url, paramObj, false, appState); - }; - - /** - * Same as #change except only changes the url's path, - * leaving the search string and such intact - * - * @param {String} path - the new path, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the path template - * @return {undefined} - */ - self.changePath = function (path, paramObj) { - self._changeLocation('path', path, paramObj); - }; - - /** - * Same as #change except that it removes the current url from history - * - * @param {String} url - the new url, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the url template - * @return {undefined} - */ - self.redirect = function (url, paramObj, appState) { - self._changeLocation('url', url, paramObj, true, appState); - }; - - /** - * Same as #redirect except only changes the url's path, - * leaving the search string and such intact - * - * @param {String} path - the new path, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the path template - * @return {undefined} - */ - self.redirectPath = function (path, paramObj) { - self._changeLocation('path', path, paramObj, true); - }; - - /** - * Evaluate a url template. templates can contain double-curly wrapped - * expressions that are evaluated in the context of the paramObj - * - * @param {String} template - the url template to evaluate - * @param {Object} [paramObj] - the variables to expose to the template - * @return {String} - the evaluated result - * @throws {Error} If any of the expressions can't be parsed. - */ - self.eval = function (template, paramObj) { - paramObj = paramObj || {}; - - return template.replace(/\{\{([^\}]+)\}\}/g, function (match, expr) { - // remove filters - const key = expr.split('|')[0].trim(); - - // verify that the expression can be evaluated - const p = $parse(key)(paramObj); - - // if evaluation can't be made, throw - if (_.isUndefined(p)) { - throw new Error( - i18n.translate('common.ui.url.replacementFailedErrorMessage', { - defaultMessage: 'Replacement failed, unresolved expression: {expr}', - values: { expr }, - }) - ); - } - - return encodeURIComponent($parse(expr)(paramObj)); - }); - }; - - /** - * convert an object's route to an href, compatible with - * window.location.href= and - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {string} - the computed href - */ - self.getRouteHref = function (obj, route) { - return '#' + self.getRouteUrl(obj, route); - }; - - /** - * convert an object's route to a url, compatible with url.change() or $location.url() - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {string} - the computed url - */ - self.getRouteUrl = function (obj, route) { - const template = obj && obj.routes && obj.routes[route]; - if (template) return self.eval(template, obj); - }; - - /** - * Similar to getRouteUrl, supports objects which list their routes, - * and redirects to the named route. See #redirect - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {undefined} - */ - self.redirectToRoute = function (obj, route) { - self.redirect(self.getRouteUrl(obj, route)); - }; - - /** - * Similar to getRouteUrl, supports objects which list their routes, - * and changes the url to the named route. See #change - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {undefined} - */ - self.changeToRoute = function (obj, route) { - self.change(self.getRouteUrl(obj, route)); - }; - - /** - * Removes the given parameter from the url. Does so without modifying the browser - * history. - * @param param - */ - self.removeParam = function (param) { - $location.search(param, null).replace(); - }; - - ///// - // private api - ///// - let reloading; - - self._changeLocation = function (type, url, paramObj, replace, appState) { - const prev = { - path: $location.path(), - search: $location.search(), - }; - - url = self.eval(url, paramObj); - $location[type](url); - if (replace) $location.replace(); - - if (appState) { - $location.search(appState.getQueryParamName(), appState.toQueryParam()); - } - - const next = { - path: $location.path(), - search: $location.search(), - }; - - if ($injector.has('$route')) { - const $route = $injector.get('$route'); - - if (self._shouldForceReload(next, prev, $route)) { - const appState = Private(AppStateProvider).getAppState(); - if (appState) appState.destroy(); - - reloading = $rootScope.$on('$locationChangeSuccess', function () { - // call the "unlisten" function returned by $on - reloading(); - reloading = false; - - $route.reload(); - }); - } - } - }; - - // determine if the router will automatically reload the route - self._shouldForceReload = function (next, prev, $route) { - if (reloading) return false; - - const route = $route.current && $route.current.$$route; - if (!route) return false; - - // for the purposes of determining whether the router will - // automatically be reloading, '' and '/' are equal - const nextPath = next.path || '/'; - const prevPath = prev.path || '/'; - if (nextPath !== prevPath) return false; - - const reloadOnSearch = route.reloadOnSearch; - const searchSame = _.isEqual(next.search, prev.search); - return (reloadOnSearch && searchSame) || !reloadOnSearch; - }; -} diff --git a/src/legacy/ui/public/utils/collection.test.ts b/src/legacy/ui/public/utils/collection.test.ts deleted file mode 100644 index 0841e3554c0d0..0000000000000 --- a/src/legacy/ui/public/utils/collection.test.ts +++ /dev/null @@ -1,141 +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 { move } from './collection'; - -describe('collection', () => { - describe('move', () => { - test('accepts previous from->to syntax', () => { - const list = [1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1]; - - expect(list[3]).toBe(1); - expect(list[8]).toBe(8); - - move(list, 8, 3); - - expect(list[8]).toBe(1); - expect(list[3]).toBe(8); - }); - - test('moves an object up based on a function callback', () => { - const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; - - expect(list[4]).toBe(0); - expect(list[5]).toBe(1); - expect(list[6]).toBe(0); - - move(list, 5, false, (v: any) => v === 0); - - expect(list[4]).toBe(1); - expect(list[5]).toBe(0); - expect(list[6]).toBe(0); - }); - - test('moves an object down based on a function callback', () => { - const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; - - expect(list[4]).toBe(0); - expect(list[5]).toBe(1); - expect(list[6]).toBe(0); - - move(list, 5, true, (v: any) => v === 0); - - expect(list[4]).toBe(0); - expect(list[5]).toBe(0); - expect(list[6]).toBe(1); - }); - - test('moves an object up based on a where callback', () => { - const list = [ - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - ]; - - expect(list[4]).toHaveProperty('v', 0); - expect(list[5]).toHaveProperty('v', 1); - expect(list[6]).toHaveProperty('v', 0); - - move(list, 5, false, { v: 0 }); - - expect(list[4]).toHaveProperty('v', 1); - expect(list[5]).toHaveProperty('v', 0); - expect(list[6]).toHaveProperty('v', 0); - }); - - test('moves an object down based on a where callback', () => { - const list = [ - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - ]; - - expect(list[4]).toHaveProperty('v', 0); - expect(list[5]).toHaveProperty('v', 1); - expect(list[6]).toHaveProperty('v', 0); - - move(list, 5, true, { v: 0 }); - - expect(list[4]).toHaveProperty('v', 0); - expect(list[5]).toHaveProperty('v', 0); - expect(list[6]).toHaveProperty('v', 1); - }); - - test('moves an object down based on a pluck callback', () => { - const list = [ - { id: 0, normal: true }, - { id: 1, normal: true }, - { id: 2, normal: true }, - { id: 3, normal: true }, - { id: 4, normal: true }, - { id: 5, normal: false }, - { id: 6, normal: true }, - { id: 7, normal: true }, - { id: 8, normal: true }, - { id: 9, normal: true }, - ]; - - expect(list[4]).toHaveProperty('id', 4); - expect(list[5]).toHaveProperty('id', 5); - expect(list[6]).toHaveProperty('id', 6); - - move(list, 5, true, 'normal'); - - expect(list[4]).toHaveProperty('id', 4); - expect(list[5]).toHaveProperty('id', 6); - expect(list[6]).toHaveProperty('id', 5); - }); - }); -}); diff --git a/src/legacy/ui/public/utils/collection.ts b/src/legacy/ui/public/utils/collection.ts deleted file mode 100644 index b882a2bbe6e5b..0000000000000 --- a/src/legacy/ui/public/utils/collection.ts +++ /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 _ from 'lodash'; - -/** - * move an obj either up or down in the collection by - * injecting it either before/after the prev/next obj that - * satisfied the qualifier - * - * or, just from one index to another... - * - * @param {array} objs - the list to move the object within - * @param {number|any} obj - the object that should be moved, or the index that the object is currently at - * @param {number|boolean} below - the index to move the object to, or whether it should be moved up or down - * @param {function} qualifier - a lodash-y callback, object = _.where, string = _.pluck - * @return {array} - the objs argument - */ -export function move( - objs: any[], - obj: object | number, - below: number | boolean, - qualifier?: ((object: object, index: number) => any) | Record | string -): object[] { - const origI = _.isNumber(obj) ? obj : objs.indexOf(obj); - if (origI === -1) { - return objs; - } - - if (_.isNumber(below)) { - // move to a specific index - objs.splice(below, 0, objs.splice(origI, 1)[0]); - return objs; - } - - below = !!below; - qualifier = qualifier && _.iteratee(qualifier); - - const above = !below; - const finder = below ? _.findIndex : _.findLastIndex; - - // find the index of the next/previous obj that meets the qualifications - const targetI = finder(objs, (otherAgg, otherI) => { - if (below && otherI <= origI) { - return; - } - if (above && otherI >= origI) { - return; - } - return Boolean(_.isFunction(qualifier) && qualifier(otherAgg, otherI)); - }); - - if (targetI === -1) { - return objs; - } - - // place the obj at it's new index - objs.splice(targetI, 0, objs.splice(origI, 1)[0]); - return objs; -} diff --git a/src/legacy/ui/public/utils/legacy_class.js b/src/legacy/ui/public/utils/legacy_class.js deleted file mode 100644 index f47650a77bb6d..0000000000000 --- a/src/legacy/ui/public/utils/legacy_class.js +++ /dev/null @@ -1,56 +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. - */ - -// create a property descriptor for properties -// that won't change -function describeConst(val) { - return { - writable: false, - enumerable: false, - configurable: false, - value: val, - }; -} - -const props = { - inherits: describeConst(function (SuperClass) { - const prototype = Object.create(SuperClass.prototype, { - constructor: describeConst(this), - superConstructor: describeConst(SuperClass), - }); - - Object.defineProperties(this, { - prototype: describeConst(prototype), - Super: describeConst(SuperClass), - }); - - return this; - }), -}; - -/** - * Add class-related behavior to a function, currently this - * only attaches an .inherits() method. - * - * @param {Constructor} ClassConstructor - The function that should be extended - * @return {Constructor} - the constructor passed in; - */ -export function createLegacyClass(ClassConstructor) { - return Object.defineProperties(ClassConstructor, props); -} 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 432c4f02bc3e6..caa0ca4890661 100644 --- a/src/legacy/ui/ui_mixin.js +++ b/src/legacy/ui/ui_mixin.js @@ -17,12 +17,8 @@ * under the License. */ -import { uiAppsMixin } from './ui_apps'; import { uiRenderMixin } from './ui_render'; -import { uiSettingsMixin } from './ui_settings'; export async function uiMixin(kbnServer) { - await kbnServer.mixin(uiAppsMixin); - await kbnServer.mixin(uiSettingsMixin); 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/index.js b/src/legacy/ui/ui_nav_links/index.js deleted file mode 100644 index e725a77ee1183..0000000000000 --- a/src/legacy/ui/ui_nav_links/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 { UiNavLink } from './ui_nav_link'; 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 23fb6028f84db..cd8dcf5aff71d 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -21,6 +21,7 @@ import { createHash } from 'crypto'; import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; +import { KibanaRequest } from '../../../core/server'; import { AppBootstrap } from './bootstrap'; import { getApmConfig } from '../apm'; @@ -79,7 +80,10 @@ export function uiRenderMixin(kbnServer, server, config) { auth: authEnabled ? { mode: 'try' } : false, }, async handler(request, h) { - const uiSettings = request.getUiSettingsService(); + const soClient = kbnServer.newPlatform.start.core.savedObjects.getScopedClient( + KibanaRequest.from(request) + ); + const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(soClient); const darkMode = !authEnabled || request.auth.isAuthenticated @@ -178,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, @@ -205,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/ui/ui_settings/index.js b/src/legacy/ui/ui_settings/index.js deleted file mode 100644 index ec3122c4e390e..0000000000000 --- a/src/legacy/ui/ui_settings/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 { uiSettingsMixin } from './ui_settings_mixin'; diff --git a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts deleted file mode 100644 index 84a64d3f46f11..0000000000000 --- a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts +++ /dev/null @@ -1,179 +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 { savedObjectsClientMock } from '../../../../core/server/mocks'; -import * as uiSettingsServiceFactoryNS from '../ui_settings_service_factory'; -import * as getUiSettingsServiceForRequestNS from '../ui_settings_service_for_request'; -// @ts-ignore -import { uiSettingsMixin } from '../ui_settings_mixin'; - -interface Decorators { - server: { [name: string]: any }; - request: { [name: string]: any }; -} - -const uiSettingDefaults = { - application: { - defaultProperty1: 'value1', - }, -}; - -describe('uiSettingsMixin()', () => { - const sandbox = sinon.createSandbox(); - - function setup() { - // maps of decorations passed to `server.decorate()` - const decorations: Decorators = { - server: {}, - request: {}, - }; - - // mock hapi server - const server = { - log: sinon.stub(), - route: sinon.stub(), - addMemoizedFactoryToRequest(name: string, factory: (...args: any[]) => any) { - this.decorate('request', name, function (this: typeof server) { - return factory(this); - }); - }, - decorate: sinon.spy((type: keyof Decorators, name: string, value: any) => { - decorations[type][name] = value; - }), - newPlatform: { - setup: { - core: { - uiSettings: { - register: sinon.stub(), - }, - }, - }, - }, - }; - - // "promise" returned from kbnServer.ready() - const readyPromise = { - then: sinon.stub(), - }; - - const kbnServer = { - server, - uiExports: { uiSettingDefaults }, - ready: sinon.stub().returns(readyPromise), - }; - - uiSettingsMixin(kbnServer, server); - - return { - kbnServer, - server, - decorations, - readyPromise, - }; - } - - afterEach(() => sandbox.restore()); - - it('passes uiSettingsDefaults to the new platform', () => { - const { server } = setup(); - sinon.assert.calledOnce(server.newPlatform.setup.core.uiSettings.register); - sinon.assert.calledWithExactly( - server.newPlatform.setup.core.uiSettings.register, - uiSettingDefaults - ); - }); - - describe('server.uiSettingsServiceFactory()', () => { - it('decorates server with "uiSettingsServiceFactory"', () => { - const { decorations } = setup(); - expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function'); - - const uiSettingsServiceFactoryStub = sandbox.stub( - uiSettingsServiceFactoryNS, - 'uiSettingsServiceFactory' - ); - sinon.assert.notCalled(uiSettingsServiceFactoryStub); - decorations.server.uiSettingsServiceFactory(); - sinon.assert.calledOnce(uiSettingsServiceFactoryStub); - }); - - it('passes `server` and `options` argument to factory', () => { - const { decorations, server } = setup(); - expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function'); - - const uiSettingsServiceFactoryStub = sandbox.stub( - uiSettingsServiceFactoryNS, - 'uiSettingsServiceFactory' - ); - - sinon.assert.notCalled(uiSettingsServiceFactoryStub); - - const savedObjectsClient = savedObjectsClientMock.create(); - decorations.server.uiSettingsServiceFactory({ - savedObjectsClient, - }); - sinon.assert.calledOnce(uiSettingsServiceFactoryStub); - sinon.assert.calledWithExactly(uiSettingsServiceFactoryStub, server as any, { - savedObjectsClient, - }); - }); - }); - - describe('request.getUiSettingsService()', () => { - it('exposes "getUiSettingsService" on requests', () => { - const { decorations } = setup(); - expect(decorations.request).to.have.property('getUiSettingsService').a('function'); - - const getUiSettingsServiceForRequestStub = sandbox.stub( - getUiSettingsServiceForRequestNS, - 'getUiSettingsServiceForRequest' - ); - sinon.assert.notCalled(getUiSettingsServiceForRequestStub); - decorations.request.getUiSettingsService(); - sinon.assert.calledOnce(getUiSettingsServiceForRequestStub); - }); - - it('passes request to getUiSettingsServiceForRequest', () => { - const { server, decorations } = setup(); - expect(decorations.request).to.have.property('getUiSettingsService').a('function'); - - const getUiSettingsServiceForRequestStub = sandbox.stub( - getUiSettingsServiceForRequestNS, - 'getUiSettingsServiceForRequest' - ); - sinon.assert.notCalled(getUiSettingsServiceForRequestStub); - const request = {}; - decorations.request.getUiSettingsService.call(request); - sinon.assert.calledWith(getUiSettingsServiceForRequestStub, server as any, request as any); - }); - }); - - describe('server.uiSettings()', () => { - it('throws an error, links to pr', () => { - const { decorations } = setup(); - expect(decorations.server).to.have.property('uiSettings').a('function'); - expect(() => { - decorations.server.uiSettings(); - }).to.throwError('http://github.com' as any); // incorrect typings - }); - }); -}); diff --git a/src/legacy/ui/ui_settings/ui_exports_consumer.js b/src/legacy/ui/ui_settings/ui_exports_consumer.js deleted file mode 100644 index d2bb3a00ce0ed..0000000000000 --- a/src/legacy/ui/ui_settings/ui_exports_consumer.js +++ /dev/null @@ -1,62 +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. - */ - -/** - * The UiExports class accepts consumer objects that it consults while - * trying to consume all of the `uiExport` declarations provided by - * plugins. - * - * UiExportConsumer is instantiated and passed to UiExports, then for - * every `uiExport` declaration the `exportConsumer(type)` method is - * with the key of the declaration. If this consumer knows how to handle - * that key we return a function that will be called with the plugins - * and values of all declarations using that key. - * - * With this, the consumer merges all of the declarations into the - * _uiSettingDefaults map, ensuring that there are not collisions along - * the way. - * - * @class UiExportsConsumer - */ -export class UiExportsConsumer { - _uiSettingDefaults = {}; - - exportConsumer(type) { - switch (type) { - case 'uiSettingDefaults': - return (plugin, settingDefinitions) => { - Object.keys(settingDefinitions).forEach((key) => { - if (key in this._uiSettingDefaults) { - throw new Error(`uiSettingDefaults for key "${key}" are already defined`); - } - - this._uiSettingDefaults[key] = settingDefinitions[key]; - }); - }; - } - } - - /** - * Get the map of uiSettingNames to "default" specifications - * @return {Object} - */ - getUiSettingDefaults() { - return this._uiSettingDefaults; - } -} diff --git a/src/legacy/ui/ui_settings/ui_settings_mixin.js b/src/legacy/ui/ui_settings/ui_settings_mixin.js deleted file mode 100644 index 8190b67732dac..0000000000000 --- a/src/legacy/ui/ui_settings/ui_settings_mixin.js +++ /dev/null @@ -1,55 +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 { uiSettingsServiceFactory } from './ui_settings_service_factory'; -import { getUiSettingsServiceForRequest } from './ui_settings_service_for_request'; - -export function uiSettingsMixin(kbnServer, server) { - const { uiSettingDefaults = {} } = kbnServer.uiExports; - const mergedUiSettingDefaults = Object.keys(uiSettingDefaults).reduce((acc, currentKey) => { - const defaultSetting = uiSettingDefaults[currentKey]; - const updatedDefaultSetting = { - ...defaultSetting, - }; - if (typeof defaultSetting.options === 'function') { - updatedDefaultSetting.options = defaultSetting.options(server); - } - if (typeof defaultSetting.value === 'function') { - updatedDefaultSetting.value = defaultSetting.value(server); - } - acc[currentKey] = updatedDefaultSetting; - return acc; - }, {}); - - server.newPlatform.setup.core.uiSettings.register(mergedUiSettingDefaults); - - server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { - return uiSettingsServiceFactory(server, options); - }); - - server.addMemoizedFactoryToRequest('getUiSettingsService', (request) => { - return getUiSettingsServiceForRequest(server, request); - }); - - server.decorate('server', 'uiSettings', () => { - throw new Error(` - server.uiSettings has been removed, see https://github.com/elastic/kibana/pull/12243. - `); - }); -} diff --git a/src/legacy/ui/ui_settings/ui_settings_service_factory.ts b/src/legacy/ui/ui_settings/ui_settings_service_factory.ts deleted file mode 100644 index 6c3c50d175dc5..0000000000000 --- a/src/legacy/ui/ui_settings/ui_settings_service_factory.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 { Legacy } from 'kibana'; -import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/server'; - -export interface UiSettingsServiceFactoryOptions { - savedObjectsClient: SavedObjectsClientContract; -} -/** - * Create an instance of UiSettingsClient that will use the - * passed `savedObjectsClient` to communicate with elasticsearch - * - * @return {IUiSettingsClient} - */ -export function uiSettingsServiceFactory( - server: Legacy.Server, - options: UiSettingsServiceFactoryOptions -): IUiSettingsClient { - return server.newPlatform.start.core.uiSettings.asScopedToClient(options.savedObjectsClient); -} diff --git a/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts b/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts deleted file mode 100644 index 057fc64c9ebd7..0000000000000 --- a/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts +++ /dev/null @@ -1,42 +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 { Legacy } from 'kibana'; -import { IUiSettingsClient } from 'src/core/server'; -import { uiSettingsServiceFactory } from './ui_settings_service_factory'; - -/** - * Get/create an instance of UiSettingsService bound to a specific request. - * Each call is cached (keyed on the request object itself) and subsequent - * requests will get the first UiSettingsService instance even if the `options` - * have changed. - * - * @param {Hapi.Server} server - * @param {Hapi.Request} request - * @param {Object} [options={}] - - * @return {IUiSettingsClient} - */ -export function getUiSettingsServiceForRequest( - server: Legacy.Server, - request: Legacy.Request -): IUiSettingsClient { - const savedObjectsClient = request.getSavedObjectsClient(); - return uiSettingsServiceFactory(server, { savedObjectsClient }); -} 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/concat_stream.js b/src/legacy/utils/streams/concat_stream.js deleted file mode 100644 index e3f8f7261d2b7..0000000000000 --- a/src/legacy/utils/streams/concat_stream.js +++ /dev/null @@ -1,46 +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 { createReduceStream } from './reduce_stream'; - -/** - * 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]" - * - * - * @param {any} initial The initial value that subsequent - * items will concat with - * @return {Transform} - */ -export function createConcatStream(initial) { - return createReduceStream((acc, chunk) => acc.concat(chunk), initial); -} diff --git a/src/legacy/utils/streams/concat_stream_providers.js b/src/legacy/utils/streams/concat_stream_providers.js deleted file mode 100644 index 11dfb84284df3..0000000000000 --- a/src/legacy/utils/streams/concat_stream_providers.js +++ /dev/null @@ -1,61 +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 { PassThrough } 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. - * - * @param {Array<() => ReadableStream>} sourceProviders - * @param {PassThroughOptions} options options passed to the PassThrough constructor - * @return {WritableStream} combined stream - */ -export function concatStreamProviders(sourceProviders, options = {}) { - 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/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/legacy/utils/streams/intersperse_stream.js b/src/legacy/utils/streams/intersperse_stream.js deleted file mode 100644 index 5f9f0b03cd7eb..0000000000000 --- a/src/legacy/utils/streams/intersperse_stream.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. - */ - -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" - * - * @param {String|Buffer} intersperseChunk - * @return {Transform} - */ -export function createIntersperseStream(intersperseChunk) { - let first = true; - - return new Transform({ - writableObjectMode: true, - readableObjectMode: true, - transform(chunk, enc, callback) { - try { - if (first) { - first = false; - } else { - this.push(intersperseChunk); - } - - this.push(chunk); - callback(null); - } catch (err) { - callback(err); - } - }, - }); -} diff --git a/src/legacy/utils/streams/list_stream.js b/src/legacy/utils/streams/list_stream.js deleted file mode 100644 index a614620b054b7..0000000000000 --- a/src/legacy/utils/streams/list_stream.js +++ /dev/null @@ -1,44 +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 } from 'stream'; - -/** - * Create a Readable stream that provides the items - * from a list as objects to subscribers - * - * @param {Array} items - the list of items to provide - * @return {Readable} - */ -export function createListStream(items = []) { - const queue = [].concat(items); - - return new Readable({ - objectMode: true, - read(size) { - queue.splice(0, size).forEach((item) => { - this.push(item); - }); - - if (!queue.length) { - this.push(null); - } - }, - }); -} diff --git a/src/legacy/utils/streams/map_stream.js b/src/legacy/utils/streams/map_stream.js deleted file mode 100644 index 4e906471330f1..0000000000000 --- a/src/legacy/utils/streams/map_stream.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 { Transform } from 'stream'; - -export function createMapStream(fn) { - let i = 0; - - return new Transform({ - objectMode: true, - async transform(value, enc, done) { - try { - this.push(await fn(value, i++)); - done(); - } catch (err) { - done(err); - } - }, - }); -} diff --git a/src/legacy/utils/streams/promise_from_streams.js b/src/legacy/utils/streams/promise_from_streams.js deleted file mode 100644 index 05f6a08aa1a09..0000000000000 --- a/src/legacy/utils/streams/promise_from_streams.js +++ /dev/null @@ -1,66 +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. - */ - -/** - * 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 } from 'stream'; - -export async function createPromiseFromStreams(streams) { - let finalChunk; - const last = streams[streams.length - 1]; - if (typeof last.read !== 'function' && 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 (typeof last.read === 'function') { - // 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) => { - pipeline(...streams, (err) => { - if (err) return reject(err); - resolve(finalChunk); - }); - }); -} diff --git a/src/legacy/utils/streams/reduce_stream.js b/src/legacy/utils/streams/reduce_stream.js deleted file mode 100644 index d66b0124d1dab..0000000000000 --- a/src/legacy/utils/streams/reduce_stream.js +++ /dev/null @@ -1,80 +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 { 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. - * - * @param {Function} - * @param {any} initial Initial value for the stream, if undefined - * then the first chunk provided is used as the - * initial value. - * @return {Transform} - */ -export function createReduceStream(reducer, initial) { - 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.js b/src/legacy/utils/streams/replace_stream.js deleted file mode 100644 index 7309bd241fa52..0000000000000 --- a/src/legacy/utils/streams/replace_stream.js +++ /dev/null @@ -1,85 +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 { Transform } from 'stream'; - -export function createReplaceStream(toReplace, replacement) { - 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, enc, 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); - } - - buffer = null; - callback(); - }, - }); -} diff --git a/src/legacy/utils/streams/split_stream.js b/src/legacy/utils/streams/split_stream.js deleted file mode 100644 index f55cbc7bd290d..0000000000000 --- a/src/legacy/utils/streams/split_stream.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. - */ - -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 - * - * @param {String} splitChunk - * @return {Transform} - */ -export function createSplitStream(splitChunk) { - let unsplitBuffer = Buffer.alloc(0); - - return new Transform({ - writableObjectMode: false, - readableObjectMode: true, - transform(chunk, enc, 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(null); - } catch (err) { - callback(err); - } - }, - - flush(callback) { - try { - this.push(unsplitBuffer.toString('utf8')); - - callback(null); - } catch (err) { - callback(err); - } - }, - }); -} diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx index 6103041cf0a4c..68a21c6a1587c 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx @@ -32,10 +32,6 @@ import { AdvancedSettingsComponent } from './advanced_settings'; import { notificationServiceMock, docLinksServiceMock } from '../../../../core/public/mocks'; import { ComponentRegistry } from '../component_registry'; -jest.mock('ui/new_platform', () => ({ - npStart: mockConfig(), -})); - jest.mock('./components/field', () => ({ Field: () => { return 'field'; 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/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts index 4343a3409b696..cd32c2025456f 100644 --- a/src/plugins/dashboard/public/application/actions/index.ts +++ b/src/plugins/dashboard/public/application/actions/index.ts @@ -42,3 +42,8 @@ export { UnlinkFromLibraryActionContext, ACTION_UNLINK_FROM_LIBRARY, } from './unlink_from_library_action'; +export { + LibraryNotificationActionContext, + LibraryNotificationAction, + ACTION_LIBRARY_NOTIFICATION, +} from './library_notification_action'; diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx new file mode 100644 index 0000000000000..385f6f14ba94c --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { isErrorEmbeddable, ReferenceOrValueEmbeddable } from '../../embeddable_plugin'; +import { DashboardContainer } from '../embeddable'; +import { getSampleDashboardInput } from '../test_helpers'; +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, +} from '../../embeddable_plugin_test_samples'; +import { coreMock } from '../../../../../core/public/mocks'; +import { CoreStart } from 'kibana/public'; +import { LibraryNotificationAction } from '.'; +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; +import { ViewMode } from '../../../../embeddable/public'; + +const { setup, doStart } = embeddablePluginMock.createInstance(); +setup.registerEmbeddableFactory( + CONTACT_CARD_EMBEDDABLE, + new ContactCardEmbeddableFactory((() => null) as any, {} as any) +); +const start = doStart(); + +let container: DashboardContainer; +let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable; +let coreStart: CoreStart; +beforeEach(async () => { + coreStart = coreMock.createStart(); + + const containerOptions = { + ExitFullScreenButton: () => null, + SavedObjectFinder: () => null, + application: {} as any, + embeddable: start, + inspector: {} as any, + notifications: {} as any, + overlays: coreStart.overlays, + savedObjectMetaData: {} as any, + uiActions: {} as any, + }; + + container = new DashboardContainer(getSampleDashboardInput(), containerOptions); + + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Kibanana', + }); + + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } + embeddable = embeddablePluginMock.mockRefOrValEmbeddable< + ContactCardEmbeddable, + ContactCardEmbeddableInput + >(contactCardEmbeddable, { + mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: contactCardEmbeddable.id }, + mockedByValueInput: { firstName: 'Kibanana', id: contactCardEmbeddable.id }, + }); + embeddable.updateInput({ viewMode: ViewMode.EDIT }); +}); + +test('Notification is shown when embeddable on dashboard has reference type input', async () => { + const action = new LibraryNotificationAction(); + embeddable.updateInput(await embeddable.getInputAsRefType()); + expect(await action.isCompatible({ embeddable })).toBe(true); +}); + +test('Notification is not shown when embeddable input is by value', async () => { + const action = new LibraryNotificationAction(); + embeddable.updateInput(await embeddable.getInputAsValueType()); + expect(await action.isCompatible({ embeddable })).toBe(false); +}); + +test('Notification is not shown when view mode is set to view', async () => { + const action = new LibraryNotificationAction(); + embeddable.updateInput(await embeddable.getInputAsRefType()); + embeddable.updateInput({ viewMode: ViewMode.VIEW }); + expect(await action.isCompatible({ embeddable })).toBe(false); +}); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx new file mode 100644 index 0000000000000..974b55275ccc1 --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -0,0 +1,89 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge } from '@elastic/eui'; +import { IEmbeddable, ViewMode, isReferenceOrValueEmbeddable } from '../../embeddable_plugin'; +import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; +import { reactToUiComponent } from '../../../../kibana_react/public'; + +export const ACTION_LIBRARY_NOTIFICATION = 'ACTION_LIBRARY_NOTIFICATION'; + +export interface LibraryNotificationActionContext { + embeddable: IEmbeddable; +} + +export class LibraryNotificationAction implements ActionByType { + public readonly id = ACTION_LIBRARY_NOTIFICATION; + public readonly type = ACTION_LIBRARY_NOTIFICATION; + public readonly order = 1; + + private displayName = i18n.translate('dashboard.panel.LibraryNotification', { + defaultMessage: 'Library', + }); + + private icon = 'folderCheck'; + + public readonly MenuItem = reactToUiComponent(() => ( + + {this.displayName} + + )); + + public getDisplayName({ embeddable }: LibraryNotificationActionContext) { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + return this.displayName; + } + + public getIconType({ embeddable }: LibraryNotificationActionContext) { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + return this.icon; + } + + public getDisplayNameTooltip = ({ embeddable }: LibraryNotificationActionContext) => { + if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { + throw new IncompatibleActionError(); + } + return i18n.translate('dashboard.panel.libraryNotification.toolTip', { + defaultMessage: + 'This panel is linked to a Library item. Editing the panel might affect other dashboards.', + }); + }; + + public isCompatible = async ({ embeddable }: LibraryNotificationActionContext) => { + return ( + embeddable.getInput()?.viewMode !== ViewMode.VIEW && + isReferenceOrValueEmbeddable(embeddable) && + embeddable.inputIsRefType(embeddable.getInput()) + ); + }; + + public execute = async () => {}; +} diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 0d20fdee07df5..212b54be9ae04 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -24,9 +24,18 @@ import { EuiCheckboxGroupIdToSelectedMap } from '@elastic/eui/src/components/for import React, { useState, ReactElement } from 'react'; import ReactDOM from 'react-dom'; import angular from 'angular'; +import deepEqual from 'fast-deep-equal'; import { Observable, pipe, Subscription, merge } from 'rxjs'; -import { filter, map, debounceTime, mapTo, startWith, switchMap } from 'rxjs/operators'; +import { + filter, + map, + debounceTime, + mapTo, + startWith, + switchMap, + distinctUntilChanged, +} from 'rxjs/operators'; import { History } from 'history'; import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; @@ -279,6 +288,12 @@ export class DashboardAppController { const updateIndexPatternsOperator = pipe( filter((container: DashboardContainer) => !!container && !isErrorEmbeddable(container)), map(getDashboardIndexPatterns), + distinctUntilChanged((a, b) => + deepEqual( + a.map((ip) => ip.id), + b.map((ip) => ip.id) + ) + ), // using switchMap for previous task cancellation switchMap((panelIndexPatterns: IndexPattern[]) => { return new Observable((observer) => { @@ -405,17 +420,29 @@ export class DashboardAppController { ) : null; }; - outputSubscription = new Subscription(); - outputSubscription.add( - dashboardContainer - .getOutput$() - .pipe( - mapTo(dashboardContainer), - startWith(dashboardContainer), // to trigger initial index pattern update - updateIndexPatternsOperator + outputSubscription = merge( + // output of dashboard container itself + dashboardContainer.getOutput$(), + // plus output of dashboard container children, + // children may change, so make sure we subscribe/unsubscribe with switchMap + dashboardContainer.getOutput$().pipe( + map(() => dashboardContainer!.getChildIds()), + distinctUntilChanged(deepEqual), + switchMap((newChildIds: string[]) => + merge( + ...newChildIds.map((childId) => + dashboardContainer!.getChild(childId).getOutput$() + ) + ) ) - .subscribe() - ); + ) + ) + .pipe( + mapTo(dashboardContainer), + startWith(dashboardContainer), // to trigger initial index pattern update + updateIndexPatternsOperator + ) + .subscribe(); inputSubscription = dashboardContainer.getInput$().subscribe(() => { let dirty = false; diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts b/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts new file mode 100644 index 0000000000000..06f380ca3862b --- /dev/null +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts @@ -0,0 +1,193 @@ +/* + * 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 { ATTRIBUTE_SERVICE_KEY } from './attribute_service'; +import { mockAttributeService } from './attribute_service_mock'; +import { coreMock } from '../../../../core/public/mocks'; + +interface TestAttributes { + title: string; + testAttr1?: string; + testAttr2?: { array: unknown[]; testAttr3: string }; +} + +interface TestByValueInput { + id: string; + [ATTRIBUTE_SERVICE_KEY]: TestAttributes; +} + +describe('attributeService', () => { + const defaultTestType = 'defaultTestType'; + let attributes: TestAttributes; + let byValueInput: TestByValueInput; + let byReferenceInput: { id: string; savedObjectId: string }; + + beforeEach(() => { + attributes = { + title: 'ultra title', + testAttr1: 'neat first attribute', + testAttr2: { array: [1, 2, 3], testAttr3: 'super attribute' }, + }; + byValueInput = { + id: '456', + attributes, + }; + byReferenceInput = { + id: '456', + savedObjectId: '123', + }; + }); + + describe('determining input type', () => { + const defaultAttributeService = mockAttributeService(defaultTestType); + const customAttributeService = mockAttributeService( + defaultTestType + ); + + it('can determine input type given default types', () => { + expect( + defaultAttributeService.inputIsRefType({ id: '456', savedObjectId: '123' }) + ).toBeTruthy(); + expect( + defaultAttributeService.inputIsRefType({ + id: '456', + attributes: { title: 'wow I am by value' }, + }) + ).toBeFalsy(); + }); + it('can determine input type given custom types', () => { + expect( + customAttributeService.inputIsRefType({ id: '456', savedObjectId: '123' }) + ).toBeTruthy(); + expect( + customAttributeService.inputIsRefType({ + id: '456', + [ATTRIBUTE_SERVICE_KEY]: { title: 'wow I am by value' }, + }) + ).toBeFalsy(); + }); + }); + + describe('unwrapping attributes', () => { + it('can unwrap all default attributes when given reference type input', async () => { + const core = coreMock.createStart(); + core.savedObjects.client.get = jest.fn().mockResolvedValueOnce({ + attributes, + }); + const attributeService = mockAttributeService( + defaultTestType, + undefined, + core + ); + expect(await attributeService.unwrapAttributes(byReferenceInput)).toEqual(attributes); + }); + + it('returns attributes when when given value type input', async () => { + const attributeService = mockAttributeService(defaultTestType); + expect(await attributeService.unwrapAttributes(byValueInput)).toEqual(attributes); + }); + + it('runs attributes through a custom unwrap method', async () => { + const core = coreMock.createStart(); + core.savedObjects.client.get = jest.fn().mockResolvedValueOnce({ + attributes, + }); + const attributeService = mockAttributeService( + defaultTestType, + { + customUnwrapMethod: (savedObject) => ({ + ...savedObject.attributes, + testAttr2: { array: [1, 2, 3, 4, 5], testAttr3: 'kibanana' }, + }), + }, + core + ); + expect(await attributeService.unwrapAttributes(byReferenceInput)).toEqual({ + ...attributes, + testAttr2: { array: [1, 2, 3, 4, 5], testAttr3: 'kibanana' }, + }); + }); + }); + + describe('wrapping attributes', () => { + it('returns given attributes when use ref type is false', async () => { + const attributeService = mockAttributeService(defaultTestType); + expect(await attributeService.wrapAttributes(attributes, false)).toEqual({ attributes }); + }); + + it('updates existing saved object with new attributes when given id', async () => { + const core = coreMock.createStart(); + const attributeService = mockAttributeService( + defaultTestType, + undefined, + core + ); + expect(await attributeService.wrapAttributes(attributes, true, byReferenceInput)).toEqual( + byReferenceInput + ); + expect(core.savedObjects.client.update).toHaveBeenCalledWith( + defaultTestType, + '123', + attributes + ); + }); + + it('creates new saved object with attributes when given no id', async () => { + const core = coreMock.createStart(); + core.savedObjects.client.create = jest.fn().mockResolvedValueOnce({ + id: '678', + }); + const attributeService = mockAttributeService( + defaultTestType, + undefined, + core + ); + expect(await attributeService.wrapAttributes(attributes, true)).toEqual({ + savedObjectId: '678', + }); + expect(core.savedObjects.client.create).toHaveBeenCalledWith(defaultTestType, attributes); + }); + + it('uses custom save method when given an id', async () => { + const customSaveMethod = jest.fn().mockReturnValue({ id: '123' }); + const attributeService = mockAttributeService(defaultTestType, { + customSaveMethod, + }); + expect(await attributeService.wrapAttributes(attributes, true, byReferenceInput)).toEqual( + byReferenceInput + ); + expect(customSaveMethod).toHaveBeenCalledWith( + defaultTestType, + attributes, + byReferenceInput.savedObjectId + ); + }); + + it('uses custom save method given no id', async () => { + const customSaveMethod = jest.fn().mockReturnValue({ id: '678' }); + const attributeService = mockAttributeService(defaultTestType, { + customSaveMethod, + }); + expect(await attributeService.wrapAttributes(attributes, true)).toEqual({ + savedObjectId: '678', + }); + expect(customSaveMethod).toHaveBeenCalledWith(defaultTestType, attributes, undefined); + }); + }); +}); diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx index fe5f6a0c8e2bd..a36363d22d87d 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -19,11 +19,16 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { EmbeddableInput, SavedObjectEmbeddableInput, isSavedObjectEmbeddableInput, IEmbeddable, + Container, + EmbeddableStart, + EmbeddableFactory, + EmbeddableFactoryNotFoundError, } from '../embeddable_plugin'; import { SavedObjectsClientContract, @@ -34,17 +39,10 @@ import { } from '../../../../core/public'; import { SavedObjectSaveModal, - showSaveModal, OnSaveProps, SaveResult, checkForDuplicateTitle, } from '../../../saved_objects/public'; -import { - EmbeddableStart, - EmbeddableFactory, - EmbeddableFactoryNotFoundError, - Container, -} from '../../../embeddable/public'; /** * The attribute service is a shared, generic service that embeddables can use to provide the functionality @@ -52,26 +50,46 @@ import { * can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object * into an embeddable input shape that contains that saved object's attributes by value. */ +export const ATTRIBUTE_SERVICE_KEY = 'attributes'; + +export interface AttributeServiceOptions { + customSaveMethod?: ( + type: string, + attributes: A, + savedObjectId?: string + ) => Promise<{ id: string }>; + customUnwrapMethod?: (savedObject: SimpleSavedObject) => A; +} + export class AttributeService< SavedObjectAttributes extends { title: string }, - ValType extends EmbeddableInput & { attributes: SavedObjectAttributes }, - RefType extends SavedObjectEmbeddableInput + ValType extends EmbeddableInput & { + [ATTRIBUTE_SERVICE_KEY]: SavedObjectAttributes; + } = EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: SavedObjectAttributes }, + RefType extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput > { - private embeddableFactory: EmbeddableFactory; + private embeddableFactory?: EmbeddableFactory; constructor( private type: string, + private showSaveModal: ( + saveModal: React.ReactElement, + I18nContext: I18nStart['Context'] + ) => void, private savedObjectsClient: SavedObjectsClientContract, private overlays: OverlayStart, private i18nContext: I18nStart['Context'], private toasts: NotificationsStart['toasts'], - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'] + getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'], + private options?: AttributeServiceOptions ) { - const factory = getEmbeddableFactory(this.type); - if (!factory) { - throw new EmbeddableFactoryNotFoundError(this.type); + if (getEmbeddableFactory) { + const factory = getEmbeddableFactory(this.type); + if (!factory) { + throw new EmbeddableFactoryNotFoundError(this.type); + } + this.embeddableFactory = factory; } - this.embeddableFactory = factory; } public async unwrapAttributes(input: RefType | ValType): Promise { @@ -79,43 +97,54 @@ export class AttributeService< const savedObject: SimpleSavedObject = await this.savedObjectsClient.get< SavedObjectAttributes >(this.type, input.savedObjectId); - return savedObject.attributes; + return this.options?.customUnwrapMethod + ? this.options?.customUnwrapMethod(savedObject) + : { ...savedObject.attributes }; } - return input.attributes; + return input[ATTRIBUTE_SERVICE_KEY]; } public async wrapAttributes( newAttributes: SavedObjectAttributes, useRefType: boolean, - embeddable?: IEmbeddable + input?: ValType | RefType ): Promise> { + const originalInput = input ? input : {}; const savedObjectId = - embeddable && isSavedObjectEmbeddableInput(embeddable.getInput()) - ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId + input && this.inputIsRefType(input) + ? (input as SavedObjectEmbeddableInput).savedObjectId : undefined; if (!useRefType) { - return { attributes: newAttributes } as ValType; - } else { - try { - if (savedObjectId) { - await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); - return { savedObjectId } as RefType; - } else { - const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); - return { savedObjectId: savedItem.id } as RefType; - } - } catch (error) { - this.toasts.addDanger({ - title: i18n.translate('dashboard.attributeService.saveToLibraryError', { - defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`, - values: { - errorMessage: error.message, - }, - }), - 'data-test-subj': 'saveDashboardFailure', - }); - return Promise.reject({ error }); + return { [ATTRIBUTE_SERVICE_KEY]: newAttributes } as ValType; + } + try { + if (this.options?.customSaveMethod) { + const savedItem = await this.options.customSaveMethod( + this.type, + newAttributes, + savedObjectId + ); + return { ...originalInput, savedObjectId: savedItem.id } as RefType; + } + + if (savedObjectId) { + await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); + return { ...originalInput, savedObjectId } as RefType; } + + const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); + return { ...originalInput, savedObjectId: savedItem.id } as RefType; + } catch (error) { + this.toasts.addDanger({ + title: i18n.translate('dashboard.attributeService.saveToLibraryError', { + defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`, + values: { + errorMessage: error.message, + }, + }), + 'data-test-subj': 'saveDashboardFailure', + }); + return Promise.reject({ error }); } } @@ -146,7 +175,7 @@ export class AttributeService< getInputAsRefType = async ( input: ValType | RefType, - saveOptions?: { showSaveModal: boolean } | { title: string } + saveOptions?: { showSaveModal: boolean; saveModalTitle?: string } | { title: string } ): Promise => { if (this.inputIsRefType(input)) { return input; @@ -159,7 +188,7 @@ export class AttributeService< copyOnSave: false, lastSavedTitle: '', getEsType: () => this.type, - getDisplayName: this.embeddableFactory.getDisplayName, + getDisplayName: this.embeddableFactory?.getDisplayName || (() => this.type), }, props.isTitleDuplicateConfirmed, props.onTitleDuplicate, @@ -169,7 +198,7 @@ export class AttributeService< } ); try { - const newAttributes = { ...input.attributes }; + const newAttributes = { ...input[ATTRIBUTE_SERVICE_KEY] }; newAttributes.title = props.newTitle; const wrappedInput = (await this.wrapAttributes(newAttributes, true)) as RefType; resolve(wrappedInput); @@ -181,11 +210,11 @@ export class AttributeService< }; if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) { - showSaveModal( + this.showSaveModal( reject()} - title={input.attributes.title} + title={get(saveOptions, 'saveModalTitle', input[ATTRIBUTE_SERVICE_KEY].title)} showCopyOnSave={false} objectType={this.type} showDescription={false} diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service_mock.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service_mock.tsx new file mode 100644 index 0000000000000..321a53361fc7a --- /dev/null +++ b/src/plugins/dashboard/public/attribute_service/attribute_service_mock.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EmbeddableInput, SavedObjectEmbeddableInput } from '../embeddable_plugin'; +import { coreMock } from '../../../../core/public/mocks'; +import { AttributeServiceOptions } from './attribute_service'; +import { CoreStart } from '../../../../core/public'; +import { AttributeService, ATTRIBUTE_SERVICE_KEY } from '..'; + +export const mockAttributeService = < + A extends { title: string }, + V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & { + [ATTRIBUTE_SERVICE_KEY]: A; + }, + R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +>( + type: string, + options?: AttributeServiceOptions, + customCore?: jest.Mocked +): AttributeService => { + const core = customCore ? customCore : coreMock.createStart(); + const service = new AttributeService( + type, + jest.fn(), + core.savedObjects.client, + core.overlays, + core.i18n.Context, + core.notifications.toasts, + jest.fn().mockReturnValue(() => ({ getDisplayName: () => type })), + options + ); + return service; +}; diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index 8a9954cc77a2e..e22d1f038a456 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -40,7 +40,7 @@ export { export { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; export { SavedObjectDashboard } from './saved_dashboards'; export { SavedDashboardPanel } from './types'; -export { AttributeService } from './attribute_service/attribute_service'; +export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './attribute_service/attribute_service'; export function plugin(initializerContext: PluginInitializerContext) { return new DashboardPlugin(initializerContext); diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 8b9b92faf9031..0ce6f9489ea02 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -40,6 +40,7 @@ import { EmbeddableStart, SavedObjectEmbeddableInput, EmbeddableInput, + PANEL_NOTIFICATION_TRIGGER, } from '../../embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public'; @@ -51,6 +52,7 @@ import { getSavedObjectFinder, SavedObjectLoader, SavedObjectsStart, + showSaveModal, } from '../../saved_objects/public'; import { ExitFullScreenButton as ExitFullScreenButtonUi, @@ -83,6 +85,12 @@ import { ACTION_UNLINK_FROM_LIBRARY, UnlinkFromLibraryActionContext, UnlinkFromLibraryAction, + ACTION_ADD_TO_LIBRARY, + AddToLibraryActionContext, + AddToLibraryAction, + ACTION_LIBRARY_NOTIFICATION, + LibraryNotificationActionContext, + LibraryNotificationAction, } from './application'; import { createDashboardUrlGenerator, @@ -96,10 +104,9 @@ import { PlaceholderEmbeddableFactory } from './application/embeddable/placehold import { UrlGeneratorState } from '../../share/public'; import { AttributeService } from '.'; import { - AddToLibraryAction, - ACTION_ADD_TO_LIBRARY, - AddToLibraryActionContext, -} from './application/actions/add_to_library_action'; + AttributeServiceOptions, + ATTRIBUTE_SERVICE_KEY, +} from './attribute_service/attribute_service'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -148,10 +155,13 @@ export interface DashboardStart { DashboardContainerByValueRenderer: ReturnType; getAttributeService: < A extends { title: string }, - V extends EmbeddableInput & { attributes: A }, - R extends SavedObjectEmbeddableInput + V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & { + [ATTRIBUTE_SERVICE_KEY]: A; + }, + R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput >( - type: string + type: string, + options?: AttributeServiceOptions ) => AttributeService; } @@ -162,6 +172,7 @@ declare module '../../../plugins/ui_actions/public' { [ACTION_CLONE_PANEL]: ClonePanelActionContext; [ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext; [ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext; + [ACTION_LIBRARY_NOTIFICATION]: LibraryNotificationActionContext; } } @@ -437,6 +448,10 @@ export class DashboardPlugin const unlinkFromLibraryAction = new UnlinkFromLibraryAction(); uiActions.registerAction(unlinkFromLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); + + const libraryNotificationAction = new LibraryNotificationAction(); + uiActions.registerAction(libraryNotificationAction); + uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); } const savedDashboardLoader = createSavedDashboardLoader({ @@ -458,14 +473,16 @@ export class DashboardPlugin DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), - getAttributeService: (type: string) => + getAttributeService: (type: string, options) => new AttributeService( type, + showSaveModal, core.savedObjects.client, core.overlays, core.i18n.Context, core.notifications.toasts, - embeddable.getEmbeddableFactory + embeddable.getEmbeddableFactory, + options ), }; } 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/plugins/data/common/index_patterns/errors.ts b/src/plugins/data/common/index_patterns/errors.ts new file mode 100644 index 0000000000000..3d92bae1968fb --- /dev/null +++ b/src/plugins/data/common/index_patterns/errors.ts @@ -0,0 +1,29 @@ +/* + * 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 { FieldSpec } from './types'; + +export class FieldTypeUnknownError extends Error { + public readonly fieldSpec: FieldSpec; + constructor(message: string, spec: FieldSpec) { + super(message); + this.name = 'FieldTypeUnknownError'; + this.fieldSpec = spec; + } +} 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 5d6ae61a77e00..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; @@ -172,16 +188,24 @@ export class IndexPattern implements IIndexPattern { }); } - private async indexFields(forceFieldRefresh: boolean = false, specs?: FieldSpec[]) { + private async indexFields(specs?: FieldSpec[]) { if (!this.id) { return; } - if (forceFieldRefresh || this.isFieldRefreshRequired(specs)) { + if (this.isFieldRefreshRequired(specs)) { 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) => { @@ -213,7 +245,7 @@ export class IndexPattern implements IIndexPattern { return this; } - private updateFromElasticSearch(response: any, forceFieldRefresh: boolean = false) { + private updateFromElasticSearch(response: any) { if (!response.found) { throw new SavedObjectNotFound(savedObjectType, this.id, 'management/kibana/indexPatterns'); } @@ -239,7 +271,7 @@ export class IndexPattern implements IIndexPattern { } this.version = response.version; - return this.indexFields(forceFieldRefresh, response.fields); + return this.indexFields(response.fields); } getComputedFields() { @@ -283,7 +315,7 @@ export class IndexPattern implements IIndexPattern { }; } - async init(forceFieldRefresh = false) { + async init() { if (!this.id) { return this; // no id === no elasticsearch document } @@ -307,7 +339,7 @@ export class IndexPattern implements IIndexPattern { }; // Do this before we attempt to update from ES since that call can potentially perform a save this.originalBody = this.prepBody(); - await this.updateFromElasticSearch(response, forceFieldRefresh); + await this.updateFromElasticSearch(response); // Do it after to ensure we have the most up to date information this.originalBody = this.prepBody(); @@ -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 2fcc6b97b1cc6..16a5586858ab9 100644 --- a/src/plugins/data/common/search/aggs/agg_type.test.ts +++ b/src/plugins/data/common/search/aggs/agg_type.test.ts @@ -99,6 +99,17 @@ describe('AggType Class', () => { expect(aggType.params[1].name).toBe('customLabel'); }); + test('disables json param', () => { + const aggType = new AggType({ + name: 'name', + title: 'title', + json: false, + }); + + expect(aggType.params.length).toBe(1); + expect(aggType.params[0].name).toBe('customLabel'); + }); + test('can disable customLabel', () => { const aggType = new AggType({ name: 'smart agg', @@ -136,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 0ba2bb66e7758..1e3839038b0f7 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -47,6 +47,7 @@ export interface AggTypeConfig< getRequestAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); getResponseAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); customLabels?: boolean; + json?: boolean; decorateAggConfig?: () => any; postFlightRequest?: ( resp: any, @@ -235,13 +236,17 @@ export class AggType< if (config.params && config.params.length && config.params[0] instanceof BaseParamType) { this.params = config.params as TParam[]; } else { - // always append the raw JSON param + // always append the raw JSON param unless it is configured to false const params: any[] = config.params ? [...config.params] : []; - params.push({ - name: 'json', - type: 'json', - advanced: true, - }); + + if (config.json !== false) { + params.push({ + name: 'json', + type: 'json', + advanced: true, + }); + } + // always append custom label if (config.customLabels !== false) { @@ -266,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/_interval_options.ts b/src/plugins/data/common/search/aggs/buckets/_interval_options.ts index 00cf50c272fa0..f94484a6edc2e 100644 --- a/src/plugins/data/common/search/aggs/buckets/_interval_options.ts +++ b/src/plugins/data/common/search/aggs/buckets/_interval_options.ts @@ -20,12 +20,15 @@ import { i18n } from '@kbn/i18n'; import { IBucketAggConfig } from './bucket_agg_type'; +export const autoInterval = 'auto'; +export const isAutoInterval = (value: unknown) => value === autoInterval; + export const intervalOptions = [ { display: i18n.translate('data.search.aggs.buckets.intervalOptions.autoDisplayName', { defaultMessage: 'Auto', }), - val: 'auto', + val: autoInterval, enabled(agg: IBucketAggConfig) { // not only do we need a time field, but the selected field needs // to be the time field. (see #3028) 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/date_histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts index 143d549836900..3d0224b213e8d 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import { createFilterDateHistogram } from './date_histogram'; -import { intervalOptions } from '../_interval_options'; +import { intervalOptions, autoInterval } from '../_interval_options'; import { AggConfigs } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; import { IBucketDateHistogramAggConfig } from '../date_histogram'; @@ -33,7 +33,10 @@ describe('AggConfig Filters', () => { let bucketStart: any; let field: any; - const init = (interval: string = 'auto', duration: any = moment.duration(15, 'minutes')) => { + const init = ( + interval: string = autoInterval, + duration: any = moment.duration(15, 'minutes') + ) => { field = { name: 'date', }; 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_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index fdf9c456b3876..c273ca53a5fed 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { KBN_FIELD_TYPES, TimeRange, TimeRangeBounds, UI_SETTINGS } from '../../../../common'; -import { intervalOptions } from './_interval_options'; +import { intervalOptions, autoInterval, isAutoInterval } from './_interval_options'; import { createFilterDateHistogram } from './create_filter/date_histogram'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -44,7 +44,7 @@ const updateTimeBuckets = ( customBuckets?: IBucketDateHistogramAggConfig['buckets'] ) => { const bounds = - agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto') + agg.params.timeRange && (agg.fieldIsTimeField() || isAutoInterval(agg.params.interval)) ? calculateBounds(agg.params.timeRange) : undefined; const buckets = customBuckets || agg.buckets; @@ -149,7 +149,7 @@ export const getDateHistogramBucketAgg = ({ return agg.getIndexPattern().timeFieldName; }, onChange(agg: IBucketDateHistogramAggConfig) { - if (get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) { + if (isAutoInterval(get(agg, 'params.interval')) && !agg.fieldIsTimeField()) { delete agg.params.interval; } }, @@ -187,7 +187,7 @@ export const getDateHistogramBucketAgg = ({ } return state; }, - default: 'auto', + default: autoInterval, options: intervalOptions, write(agg, output, aggs) { updateTimeBuckets(agg, calculateBounds); 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/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index 3727747984d3e..a8ac72c174c72 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -103,7 +103,28 @@ describe('Histogram Agg', () => { }); }); + describe('maxBars', () => { + test('should not be written to the DSL', () => { + const aggConfigs = getAggConfigs({ + maxBars: 50, + field: { + name: 'field', + }, + }); + const { [BUCKET_TYPES.HISTOGRAM]: params } = aggConfigs.aggs[0].toDsl(); + + expect(params).not.toHaveProperty('maxBars'); + }); + }); + describe('interval', () => { + test('accepts "auto" value', () => { + const params = getParams({ + interval: 'auto', + }); + + expect(params).toHaveProperty('interval', 1); + }); test('accepts a whole number', () => { const params = getParams({ interval: 100, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index 2b263013e55a2..4b631e1fd7cd7 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -28,6 +28,8 @@ import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; import { BUCKET_TYPES } from './bucket_agg_types'; import { ExtendedBounds } from './lib/extended_bounds'; +import { isAutoInterval, autoInterval } from './_interval_options'; +import { calculateHistogramInterval } from './lib/histogram_calculate_interval'; export interface AutoBounds { min: number; @@ -47,6 +49,7 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig { export interface AggParamsHistogram extends BaseAggParams { field: string; interval: string; + maxBars?: number; intervalBase?: number; min_doc_count?: boolean; has_extended_bounds?: boolean; @@ -102,6 +105,7 @@ export const getHistogramBucketAgg = ({ }, { name: 'interval', + default: autoInterval, modifyAggConfigOnSearchRequestStart( aggConfig: IBucketHistogramAggConfig, searchSource: any, @@ -127,9 +131,12 @@ export const getHistogramBucketAgg = ({ return childSearchSource .fetch(options) .then((resp: any) => { + const min = resp.aggregations?.minAgg?.value ?? 0; + const max = resp.aggregations?.maxAgg?.value ?? 0; + aggConfig.setAutoBounds({ - min: get(resp, 'aggregations.minAgg.value'), - max: get(resp, 'aggregations.maxAgg.value'), + min, + max, }); }) .catch((e: Error) => { @@ -143,46 +150,24 @@ export const getHistogramBucketAgg = ({ }); }, write(aggConfig, output) { - let interval = parseFloat(aggConfig.params.interval); - if (interval <= 0) { - interval = 1; - } - const autoBounds = aggConfig.getAutoBounds(); - - // ensure interval does not create too many buckets and crash browser - if (autoBounds) { - const range = autoBounds.max - autoBounds.min; - const bars = range / interval; - - if (bars > getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS)) { - const minInterval = range / getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS); - - // Round interval by order of magnitude to provide clean intervals - // Always round interval up so there will always be less buckets than histogram:maxBars - const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval))); - let roundInterval = orderOfMagnitude; - - while (roundInterval < minInterval) { - roundInterval += orderOfMagnitude; - } - interval = roundInterval; - } - } - const base = aggConfig.params.intervalBase; - - if (base) { - if (interval < base) { - // In case the specified interval is below the base, just increase it to it's base - interval = base; - } else if (interval % base !== 0) { - // In case the interval is not a multiple of the base round it to the next base - interval = Math.round(interval / base) * base; - } - } - - output.params.interval = interval; + const values = aggConfig.getAutoBounds(); + + output.params.interval = calculateHistogramInterval({ + values, + interval: aggConfig.params.interval, + maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), + maxBucketsUserInput: aggConfig.params.maxBars, + intervalBase: aggConfig.params.intervalBase, + }); }, }, + { + name: 'maxBars', + shouldShow(agg) { + return isAutoInterval(get(agg, 'params.interval')); + }, + write: () => {}, + }, { name: 'min_doc_count', default: false, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts index 34b6fa1a6dcd6..354946f99a2f5 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts @@ -43,6 +43,7 @@ describe('agg_expression_functions', () => { "interval": "10", "intervalBase": undefined, "json": undefined, + "maxBars": undefined, "min_doc_count": undefined, }, "schema": undefined, @@ -55,8 +56,9 @@ describe('agg_expression_functions', () => { test('includes optional params when they are provided', () => { const actual = fn({ field: 'field', - interval: '10', + interval: 'auto', intervalBase: 1, + maxBars: 25, min_doc_count: false, has_extended_bounds: false, extended_bounds: JSON.stringify({ @@ -77,9 +79,10 @@ describe('agg_expression_functions', () => { }, "field": "field", "has_extended_bounds": false, - "interval": "10", + "interval": "auto", "intervalBase": 1, "json": undefined, + "maxBars": 25, "min_doc_count": false, }, "schema": undefined, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts index 877fd13e59f87..2e833bbe0a3eb 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts @@ -85,6 +85,12 @@ export const aggHistogram = (): FunctionDefinition => ({ defaultMessage: 'Specifies whether to use min_doc_count for this aggregation', }), }, + maxBars: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.histogram.maxBars.help', { + defaultMessage: 'Calculate interval to get approximately this many bars', + }), + }, has_extended_bounds: { types: ['boolean'], help: i18n.translate('data.search.aggs.buckets.histogram.hasExtendedBounds.help', { 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/lib/histogram_calculate_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts new file mode 100644 index 0000000000000..fd788d3339295 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts @@ -0,0 +1,144 @@ +/* + * 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 { + calculateHistogramInterval, + CalculateHistogramIntervalParams, +} from './histogram_calculate_interval'; + +describe('calculateHistogramInterval', () => { + describe('auto calculating mode', () => { + let params: CalculateHistogramIntervalParams; + + beforeEach(() => { + params = { + interval: 'auto', + intervalBase: undefined, + maxBucketsUiSettings: 100, + maxBucketsUserInput: undefined, + values: { + min: 0, + max: 1, + }, + }; + }); + + describe('maxBucketsUserInput is defined', () => { + test('should not set interval which more than largest possible', () => { + const p = { + ...params, + maxBucketsUserInput: 200, + values: { + min: 150, + max: 250, + }, + }; + expect(calculateHistogramInterval(p)).toEqual(1); + }); + + test('should correctly work for float numbers (small numbers)', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: 50, + values: { + min: 0.1, + max: 0.9, + }, + }) + ).toBe(0.02); + }); + + test('should correctly work for float numbers (big numbers)', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: 10, + values: { + min: 10.45, + max: 1000.05, + }, + }) + ).toBe(100); + }); + }); + + describe('maxBucketsUserInput is not defined', () => { + test('should not set interval which more than largest possible', () => { + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 0, + max: 100, + }, + }) + ).toEqual(1); + }); + + test('should set intervals for integer numbers (diff less than maxBucketsUiSettings)', () => { + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 1, + max: 10, + }, + }) + ).toEqual(0.1); + }); + + test('should set intervals for integer numbers (diff more than maxBucketsUiSettings)', () => { + // diff === 44445; interval === 500; buckets === 89 + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 45678, + max: 90123, + }, + }) + ).toEqual(500); + }); + + test('should set intervals the same for the same interval', () => { + // both diffs are the same + // diff === 1.655; interval === 0.02; buckets === 82 + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 1.245, + max: 2.9, + }, + }) + ).toEqual(0.02); + expect( + calculateHistogramInterval({ + ...params, + values: { + min: 0.5, + max: 2.3, + }, + }) + ).toEqual(0.02); + }); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts new file mode 100644 index 0000000000000..f4e42fa8881ef --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts @@ -0,0 +1,143 @@ +/* + * 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 { isAutoInterval } from '../_interval_options'; + +interface IntervalValuesRange { + min: number; + max: number; +} + +export interface CalculateHistogramIntervalParams { + interval: string; + maxBucketsUiSettings: number; + maxBucketsUserInput?: number; + intervalBase?: number; + values?: IntervalValuesRange; +} + +/** + * Round interval by order of magnitude to provide clean intervals + */ +const roundInterval = (minInterval: number) => { + const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval))); + let interval = orderOfMagnitude; + + while (interval < minInterval) { + interval += orderOfMagnitude; + } + + return interval; +}; + +const calculateForGivenInterval = ( + diff: number, + interval: number, + maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'] +) => { + const bars = diff / interval; + + if (bars > maxBucketsUiSettings) { + const minInterval = diff / maxBucketsUiSettings; + + return roundInterval(minInterval); + } + + return interval; +}; + +/** + * Algorithm for determining auto-interval + + 1. Define maxBars as Math.min(, ) + 2. Find the min and max values in the data + 3. Subtract the min from max to get diff + 4. Set exactInterval to diff / maxBars + 5. Based on exactInterval, find the power of 10 that's lower and higher + 6. Find the number of expected buckets that lowerPower would create: diff / lowerPower + 7. Find the number of expected buckets that higherPower would create: diff / higherPower + 8. There are three possible final intervals, pick the one that's closest to maxBars: + - The lower power of 10 + - The lower power of 10, times 2 + - The lower power of 10, times 5 + **/ +const calculateAutoInterval = ( + diff: number, + maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'], + maxBucketsUserInput: CalculateHistogramIntervalParams['maxBucketsUserInput'] +) => { + const maxBars = Math.min(maxBucketsUiSettings, maxBucketsUserInput ?? maxBucketsUiSettings); + const exactInterval = diff / maxBars; + + const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); + + const autoBuckets = diff / lowerPower; + + if (autoBuckets > maxBars) { + if (autoBuckets / 2 <= maxBars) { + return lowerPower * 2; + } else if (autoBuckets / 5 <= maxBars) { + return lowerPower * 5; + } else { + return lowerPower * 10; + } + } + + return lowerPower; +}; + +export const calculateHistogramInterval = ({ + interval, + maxBucketsUiSettings, + maxBucketsUserInput, + intervalBase, + values, +}: CalculateHistogramIntervalParams) => { + const isAuto = isAutoInterval(interval); + let calculatedInterval = isAuto ? 0 : parseFloat(interval); + + // should return NaN on non-numeric or invalid values + if (Number.isNaN(calculatedInterval)) { + return calculatedInterval; + } + + if (values) { + const diff = values.max - values.min; + + if (diff) { + calculatedInterval = isAuto + ? calculateAutoInterval(diff, maxBucketsUiSettings, maxBucketsUserInput) + : calculateForGivenInterval(diff, calculatedInterval, maxBucketsUiSettings); + } + } + + if (intervalBase) { + if (calculatedInterval < intervalBase) { + // In case the specified interval is below the base, just increase it to it's base + calculatedInterval = intervalBase; + } else if (calculatedInterval % intervalBase !== 0) { + // In case the interval is not a multiple of the base round it to the next base + calculatedInterval = Math.round(calculatedInterval / intervalBase) * intervalBase; + } + } + + const defaultValueForUnspecifiedInterval = 1; + + return calculatedInterval || defaultValueForUnspecifiedInterval; +}; diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts index ae7630ecd3dac..04e64233ce196 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -20,6 +20,7 @@ import moment from 'moment'; import { TimeBuckets, TimeBucketsConfig } from './time_buckets'; +import { autoInterval } from '../../_interval_options'; describe('TimeBuckets', () => { const timeBucketConfig: TimeBucketsConfig = { @@ -103,7 +104,7 @@ describe('TimeBuckets', () => { test('setInterval/getInterval - intreval is a "auto"', () => { const timeBuckets = new TimeBuckets(timeBucketConfig); - timeBuckets.setInterval('auto'); + timeBuckets.setInterval(autoInterval); const interval = timeBuckets.getInterval(); expect(interval.description).toEqual('0 milliseconds'); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts index 6402a6e83ead9..d054df0c9274e 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -28,6 +28,7 @@ import { convertIntervalToEsInterval, EsInterval, } from './calc_es_interval'; +import { autoInterval } from '../../_interval_options'; interface TimeBucketsInterval extends moment.Duration { // TODO double-check whether all of these are needed @@ -189,8 +190,8 @@ export class TimeBuckets { interval = input.val; } - if (!interval || interval === 'auto') { - this._i = 'auto'; + if (!interval || interval === autoInterval) { + this._i = autoInterval; return; } 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/count.ts b/src/plugins/data/common/search/aggs/metrics/count.ts index d990599586e81..9c9f36651f4d2 100644 --- a/src/plugins/data/common/search/aggs/metrics/count.ts +++ b/src/plugins/data/common/search/aggs/metrics/count.ts @@ -28,6 +28,7 @@ export const getCountMetricAgg = () => defaultMessage: 'Count', }), hasNoDsl: true, + json: false, makeLabel() { return i18n.translate('data.search.aggs.metrics.countLabel', { defaultMessage: 'Count', diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts index 846feb9296fca..32189f07581e6 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts @@ -34,7 +34,6 @@ describe('agg_expression_functions', () => { "id": undefined, "params": Object { "customLabel": undefined, - "json": undefined, }, "schema": undefined, "type": "count", @@ -42,18 +41,5 @@ describe('agg_expression_functions', () => { } `); }); - - test('correctly parses json string argument', () => { - const actual = fn({ - json: '{ "foo": true }', - }); - - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); - }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.ts index 338ca18209299..7d4616ffdc619 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; const fnName = 'aggCount'; @@ -55,12 +54,6 @@ export const aggCount = (): FunctionDefinition => ({ defaultMessage: 'Schema to use for this aggregation', }), }, - json: { - types: ['string'], - help: i18n.translate('data.search.aggs.metrics.count.json.help', { - defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', - }), - }, customLabel: { types: ['string'], help: i18n.translate('data.search.aggs.metrics.count.customLabel.help', { @@ -78,10 +71,7 @@ export const aggCount = (): FunctionDefinition => ({ enabled, schema, type: METRIC_TYPES.COUNT, - params: { - ...rest, - json: getParsedValue(args, 'json'), - }, + params: rest, }, }; }, 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/aggs/utils/calculate_auto_time_expression.ts b/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts index 622e8101f34ab..3637ded44c50a 100644 --- a/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts +++ b/src/plugins/data/common/search/aggs/utils/calculate_auto_time_expression.ts @@ -21,6 +21,7 @@ import { UI_SETTINGS } from '../../../../common/constants'; import { TimeRange } from '../../../../common/query'; import { TimeBuckets } from '../buckets/lib/time_buckets'; import { toAbsoluteDates } from './date_interval_utils'; +import { autoInterval } from '../buckets/_interval_options'; export function getCalculateAutoTimeExpression(getConfig: (key: string) => any) { return function calculateAutoTimeExpression(range: TimeRange) { @@ -36,7 +37,7 @@ export function getCalculateAutoTimeExpression(getConfig: (key: string) => any) 'dateFormat:scaled': getConfig('dateFormat:scaled'), }); - buckets.setInterval('auto'); + buckets.setInterval(autoInterval); buckets.setBounds({ min: moment(dates.from), max: moment(dates.to), 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/plugins/data/public/field_formats/field_formats_registry.stub.ts b/src/plugins/data/public/field_formats/field_formats_registry.stub.ts new file mode 100644 index 0000000000000..e8741ca41036b --- /dev/null +++ b/src/plugins/data/public/field_formats/field_formats_registry.stub.ts @@ -0,0 +1,36 @@ +/* + * 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 { 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(); + const getConfig = core.uiSettings.get.bind(core.uiSettings); + + fieldFormatsRegistry.init(getConfig, {}, baseFormattersPublic); + + fieldFormatsRegistry.deserialize = deserializeFieldFormat.bind( + fieldFormatsRegistry as DataPublicPluginStart['fieldFormats'] + ); + + return 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 51f4fc7ce94b9..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,10 +17,8 @@ * 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/'); }); - -jest.doMock('ui/new_platform', () => ({ npSetup: { core: { http } } })); 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 de4ec58dfdab3..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; } @@ -1006,7 +968,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) id?: string; // (undocumented) - init(forceFieldRefresh?: boolean): Promise; + init(): Promise; // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -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 = >; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; diff --git a/src/plugins/data/public/search/expressions/create_filter.test.ts b/src/plugins/data/public/search/expressions/create_filter.test.ts index 7968c80628531..7cc336a1c20e9 100644 --- a/src/plugins/data/public/search/expressions/create_filter.test.ts +++ b/src/plugins/data/public/search/expressions/create_filter.test.ts @@ -52,6 +52,7 @@ describe('createFilter', () => { 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/es_client/index.ts b/src/plugins/data/public/search/legacy/es_client/index.ts deleted file mode 100644 index 78ac83af642d8..0000000000000 --- a/src/plugins/data/public/search/legacy/es_client/index.ts +++ /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 { getEsClient } from './get_es_client'; -export { LegacyApiCaller } from './types'; diff --git a/src/plugins/data/public/search/legacy/es_client/types.ts b/src/plugins/data/public/search/legacy/es_client/types.ts deleted file mode 100644 index 2d35188322a4e..0000000000000 --- a/src/plugins/data/public/search/legacy/es_client/types.ts +++ /dev/null @@ -1,30 +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 { SearchResponse } from 'elasticsearch'; -import { SearchRequest } from '../../fetch'; - -export interface LegacyApiCaller { - search: (searchRequest: SearchRequest) => LegacyApiCallerResponse; - msearch: (searchRequest: SearchRequest) => LegacyApiCallerResponse; -} - -interface LegacyApiCallerResponse extends Promise> { - abort: () => void; -} 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/plugins/data/public/test_utils.ts b/src/plugins/data/public/test_utils.ts new file mode 100644 index 0000000000000..f04b20e96fa1d --- /dev/null +++ b/src/plugins/data/public/test_utils.ts @@ -0,0 +1,20 @@ +/* + * 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 { getFieldFormatsRegistry } from './field_formats/field_formats_registry.stub'; diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 86ee98b7af9d8..2d311fd88eb39 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -17,8 +17,7 @@ * under the License. */ -import { Component } from 'react'; -import React from 'react'; +import React, { Component, RefObject, createRef } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -30,6 +29,7 @@ import { EuiButton, EuiLink, htmlIdGenerator, + EuiPortal, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -42,6 +42,7 @@ import { withKibana, KibanaReactContextValue, toMountPoint } from '../../../../k import { fetchIndexPatterns } from './fetch_index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; +import { SuggestionsListSize } from '../typeahead/suggestions_component'; import { SuggestionsComponent } from '..'; interface Props { @@ -60,6 +61,7 @@ interface Props { onChangeQueryInputFocus?: (isFocused: boolean) => void; onSubmit?: (query: Query) => void; dataTestSubj?: string; + size?: SuggestionsListSize; } interface State { @@ -70,6 +72,7 @@ interface State { selectionStart: number | null; selectionEnd: number | null; indexPatterns: IIndexPattern[]; + queryBarRect: DOMRect | undefined; } const KEY_CODES = { @@ -93,6 +96,7 @@ export class QueryStringInputUI extends Component { selectionStart: null, selectionEnd: null, indexPatterns: [], + queryBarRect: undefined, }; public inputRef: HTMLTextAreaElement | null = null; @@ -101,6 +105,7 @@ export class QueryStringInputUI extends Component { private abortController?: AbortController; private services = this.props.kibana.services; private componentIsUnmounting = false; + private queryBarInputDivRefInstance: RefObject = createRef(); private getQueryString = () => { return toUser(this.props.query.query); @@ -494,8 +499,13 @@ export class QueryStringInputUI extends Component { this.initPersistedLog(); this.fetchIndexPatterns().then(this.updateSuggestions); + this.handleListUpdate(); window.addEventListener('resize', this.handleAutoHeight); + window.addEventListener('scroll', this.handleListUpdate, { + passive: true, // for better performance as we won't call preventDefault + capture: true, // scroll events don't bubble, they must be captured instead + }); } public componentDidUpdate(prevProps: Props) { @@ -533,12 +543,19 @@ export class QueryStringInputUI extends Component { this.updateSuggestions.cancel(); this.componentIsUnmounting = true; window.removeEventListener('resize', this.handleAutoHeight); + window.removeEventListener('scroll', this.handleListUpdate); } + handleListUpdate = () => + this.setState({ + queryBarRect: this.queryBarInputDivRefInstance.current?.getBoundingClientRect(), + }); + handleAutoHeight = () => { if (this.inputRef !== null && document.activeElement === this.inputRef) { this.inputRef.style.setProperty('height', `${this.inputRef.scrollHeight}px`, 'important'); } + this.handleListUpdate(); }; handleRemoveHeight = () => { @@ -587,6 +604,7 @@ export class QueryStringInputUI extends Component {

- - + + + diff --git a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap index 3b51c1db50d00..2fa7834872f6b 100644 --- a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap +++ b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap @@ -1,17 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SuggestionsComponent Passing the index should control which suggestion is selected 1`] = ` -
-
+ `; exports[`SuggestionsComponent Should display given suggestions if the show prop is true 1`] = ` -
-
+ `; diff --git a/src/plugins/data/public/ui/typeahead/_suggestion.scss b/src/plugins/data/public/ui/typeahead/_suggestion.scss index 81c05f1a8a78c..67ff17d017053 100644 --- a/src/plugins/data/public/ui/typeahead/_suggestion.scss +++ b/src/plugins/data/public/ui/typeahead/_suggestion.scss @@ -6,28 +6,36 @@ $kbnTypeaheadTypes: ( conjunction: $euiColorVis3, ); +.kbnTypeahead.kbnTypeahead--small { + max-height: 20vh; +} + +.kbnTypeahead__popover--top { + @include euiBottomShadowFlat; + border-top-left-radius: $euiBorderRadius; + border-top-right-radius: $euiBorderRadius; +} + +.kbnTypeahead__popover--bottom { + @include euiBottomShadow($adjustBorders: true); + border-bottom-left-radius: $euiBorderRadius; + border-bottom-right-radius: $euiBorderRadius; +} + .kbnTypeahead { - position: relative; + max-height: 60vh; .kbnTypeahead__popover { - @include euiBottomShadow($adjustBorders: true); + max-height: inherit; + @include euiScrollBar; border: 1px solid; border-color: $euiBorderColor; color: $euiTextColor; background-color: $euiColorEmptyShade; - position: absolute; - top: -2px; + position: relative; z-index: $euiZContentMenu; width: 100%; - border-bottom-left-radius: $euiBorderRadius; - border-bottom-right-radius: $euiBorderRadius; - - .kbnTypeahead__items { - @include euiScrollBar; - - max-height: 60vh; - overflow-y: auto; - } + overflow-y: auto; .kbnTypeahead__item { height: $euiSizeXL; diff --git a/src/plugins/data/public/ui/typeahead/constants.ts b/src/plugins/data/public/ui/typeahead/constants.ts new file mode 100644 index 0000000000000..08f9bd23e16f3 --- /dev/null +++ b/src/plugins/data/public/ui/typeahead/constants.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ + +/** + * Minimum width in px to display suggestion description correctly + * @public + */ +export const SUGGESTIONS_LIST_REQUIRED_WIDTH = 600; + +/** + * Minimum bottom distance in px to display list of suggestions + * @public + */ +export const SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE = 250; + +/** + * A distance in px to display suggestions list right under the query input without a gap + * @public + */ +export const SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET = 2; diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx index 9fe33b003527e..ba78bdd802601 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx @@ -44,6 +44,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); @@ -59,6 +60,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); @@ -79,6 +81,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={innerRefCallback} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); }); @@ -94,6 +97,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); @@ -113,6 +117,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx index b859428e6ed7e..724287b874bf7 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx @@ -46,6 +46,7 @@ interface Props { suggestion: QuerySuggestion; innerRef: (node: HTMLDivElement) => void; ariaId: string; + shouldDisplayDescription: boolean; } export function SuggestionComponent(props: Props) { @@ -72,7 +73,9 @@ export function SuggestionComponent(props: Props) {
{props.suggestion.text}
-
{props.suggestion.description}
+ {props.shouldDisplayDescription && ( +
{props.suggestion.description}
+ )}
); diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index 011a729c6a616..583940015c152 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -54,6 +54,7 @@ describe('SuggestionsComponent', () => { show={false} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -69,6 +70,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={[]} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -84,6 +86,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -100,6 +103,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -116,6 +120,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -134,6 +139,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx index 77dd7dcec01ee..dc7c55374f1d5 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -19,8 +19,15 @@ import { isEmpty } from 'lodash'; import React, { Component } from 'react'; +import classNames from 'classnames'; +import styled from 'styled-components'; import { QuerySuggestion } from '../../autocomplete'; import { SuggestionComponent } from './suggestion_component'; +import { + SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE, + SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET, + SUGGESTIONS_LIST_REQUIRED_WIDTH, +} from './constants'; interface Props { index: number | null; @@ -29,18 +36,24 @@ interface Props { show: boolean; suggestions: QuerySuggestion[]; loadMore: () => void; + queryBarRect?: DOMRect; + size?: SuggestionsListSize; } +export type SuggestionsListSize = 's' | 'l'; + export class SuggestionsComponent extends Component { private childNodes: HTMLDivElement[] = []; private parentNode: HTMLDivElement | null = null; public render() { - if (!this.props.show || isEmpty(this.props.suggestions)) { + if (!this.props.queryBarRect || !this.props.show || isEmpty(this.props.suggestions)) { return null; } const suggestions = this.props.suggestions.map((suggestion, index) => { + const isDescriptionFittable = + this.props.queryBarRect!.width >= SUGGESTIONS_LIST_REQUIRED_WIDTH; return ( (this.childNodes[index] = node)} @@ -50,17 +63,38 @@ export class SuggestionsComponent extends Component { onMouseEnter={() => this.props.onMouseEnter(index)} ariaId={'suggestion-' + index} key={`${suggestion.type} - ${suggestion.text}`} + shouldDisplayDescription={isDescriptionFittable} /> ); }); + const documentHeight = document.documentElement.clientHeight || window.innerHeight; + const { queryBarRect } = this.props; + + // reflects if the suggestions list has enough space below to be opened down + const isSuggestionsListFittable = + documentHeight - (queryBarRect.top + queryBarRect.height) > + SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE; + const verticalListPosition = isSuggestionsListFittable + ? `top: ${window.scrollY + queryBarRect.bottom - SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET}px;` + : `bottom: ${documentHeight - (window.scrollY + queryBarRect.top)}px;`; + return ( -
-
-
+ +
+
(this.parentNode = node)} onScroll={this.handleScroll} @@ -69,7 +103,7 @@ export class SuggestionsComponent extends Component {
-
+ ); } @@ -116,3 +150,11 @@ export class SuggestionsComponent extends Component { } }; } + +const StyledSuggestionsListDiv = styled.div` + ${(props: { queryBarRect: DOMRect; verticalListPosition: string }) => ` + position: absolute; + left: ${props.queryBarRect.left}px; + width: ${props.queryBarRect.width}px; + ${props.verticalListPosition}`} +`; diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index c3b06992dba0e..f300fb0779e38 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, // tabify @@ -208,7 +209,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/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts deleted file mode 100644 index d91aeee1fe818..0000000000000 --- a/src/plugins/data/server/search/routes.test.ts +++ /dev/null @@ -1,91 +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 { 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'; - -describe('Search service', () => { - let mockDataStart: MockedKeys; - let mockCoreSetup: MockedKeys>; - - beforeEach(() => { - mockDataStart = dataPluginMock.createStartContract(); - mockCoreSetup = coreMock.createSetup({ pluginStartContract: mockDataStart }); - }); - - it('handler calls context.search.search with the given request and strategy', async () => { - const response = { id: 'yay' }; - mockDataStart.search.search.mockResolvedValue(response); - const mockContext = {}; - const mockBody = { id: undefined, params: {} }; - const mockParams = { strategy: 'foo' }; - const mockRequest = httpServerMock.createKibanaRequest({ - body: mockBody, - params: mockParams, - }); - const mockResponse = httpServerMock.createResponseFactory(); - - registerSearchRoute(mockCoreSetup); - - 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(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); - 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 () => { - mockDataStart.search.search.mockRejectedValue({ - message: 'oh no', - body: { - error: 'oops', - }, - }); - - const mockContext = {}; - const mockBody = { id: undefined, params: {} }; - const mockParams = { strategy: 'foo' }; - const mockRequest = httpServerMock.createKibanaRequest({ - body: mockBody, - params: mockParams, - }); - const mockResponse = httpServerMock.createResponseFactory(); - - registerSearchRoute(mockCoreSetup); - - 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(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); - 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'); - }); -}); diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts deleted file mode 100644 index 3d813f745305f..0000000000000 --- a/src/plugins/data/server/search/routes.ts +++ /dev/null @@ -1,108 +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 { 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(); - - router.post( - { - path: '/internal/search/{strategy}/{id?}', - validate: { - params: schema.object({ - strategy: schema.string(), - id: schema.maybe(schema.string()), - }), - - query: schema.object({}, { unknowns: 'allow' }), - - body: schema.object({}, { unknowns: 'allow' }), - }, - }, - async (context, request, res) => { - const searchRequest = request.body; - const { strategy, id } = request.params; - const signal = getRequestAbortedSignal(request.events.aborted$); - - const [, , selfStart] = await core.getStartServices(); - - try { - const response = await selfStart.search.search( - context, - { ...searchRequest, id }, - { - signal, - strategy, - } - ); - 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, - }, - }, - }); - } - } - ); - - router.delete( - { - path: '/internal/search/{strategy}/{id}', - validate: { - params: schema.object({ - strategy: schema.string(), - id: schema.string(), - }), - - query: schema.object({}, { unknowns: 'allow' }), - }, - }, - async (context, request, res) => { - const { strategy, id } = request.params; - - const [, , selfStart] = await core.getStartServices(); - const searchStrategy = selfStart.search.getSearchStrategy(strategy); - if (!searchStrategy.cancel) return res.ok(); - - try { - await searchStrategy.cancel(context, id); - return res.ok(); - } catch (err) { - return res.customError({ - statusCode: err.statusCode, - body: { - message: err.message, - attributes: { - error: err.body.error, - }, - }, - }); - } - } - ); -} diff --git a/src/plugins/data/server/search/routes/index.ts b/src/plugins/data/server/search/routes/index.ts new file mode 100644 index 0000000000000..2217890ff778e --- /dev/null +++ b/src/plugins/data/server/search/routes/index.ts @@ -0,0 +1,21 @@ +/* + * 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 * 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/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts new file mode 100644 index 0000000000000..e2518acd7d505 --- /dev/null +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { registerSearchRoute } from './search'; +import { DataPluginStart } from '../../plugin'; +import { dataPluginMock } from '../../mocks'; + +describe('Search service', () => { + 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 context.search.search with the given request and strategy', async () => { + const response = { id: 'yay' }; + mockDataStart.search.search.mockResolvedValue(response); + const mockContext = {}; + const mockBody = { id: undefined, params: {} }; + const mockParams = { strategy: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + params: mockParams, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerSearchRoute(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(mockDataStart.search.search).toBeCalled(); + expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); + 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 () => { + mockDataStart.search.search.mockRejectedValue({ + message: 'oh no', + body: { + error: 'oops', + }, + }); + + const mockContext = {}; + const mockBody = { id: undefined, params: {} }; + const mockParams = { strategy: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + params: mockParams, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerSearchRoute(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(mockDataStart.search.search).toBeCalled(); + expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); + 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'); + }); +}); diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts new file mode 100644 index 0000000000000..4340285583489 --- /dev/null +++ b/src/plugins/data/server/search/routes/search.ts @@ -0,0 +1,109 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +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?}', + validate: { + params: schema.object({ + strategy: schema.string(), + id: schema.maybe(schema.string()), + }), + + query: schema.object({}, { unknowns: 'allow' }), + + body: schema.object({}, { unknowns: 'allow' }), + }, + }, + async (context, request, res) => { + const searchRequest = request.body; + const { strategy, id } = request.params; + const abortSignal = getRequestAbortedSignal(request.events.aborted$); + + const [, , selfStart] = await getStartServices(); + + try { + const response = await selfStart.search.search( + context, + { ...searchRequest, id }, + { + abortSignal, + strategy, + } + ); + 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, + }, + }, + }); + } + } + ); + + router.delete( + { + path: '/internal/search/{strategy}/{id}', + validate: { + params: schema.object({ + strategy: schema.string(), + id: schema.string(), + }), + + query: schema.object({}, { unknowns: 'allow' }), + }, + }, + async (context, request, res) => { + const { strategy, id } = request.params; + + const [, , selfStart] = await getStartServices(); + const searchStrategy = selfStart.search.getSearchStrategy(strategy); + if (!searchStrategy.cancel) return res.ok(); + + try { + await searchStrategy.cancel(context, id); + return res.ok(); + } catch (err) { + return res.customError({ + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + error: err.body.error, + }, + }, + }); + } + } + ); +} 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 5ce1bb3e6b9f8..6ce8430d0573b 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -18,7 +18,7 @@ */ import { RequestHandlerContext } from '../../../../core/server'; -import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; +import { IKibanaSearchResponse, IKibanaSearchRequest, ISearchOptions } from '../../common/search'; import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors/usage'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; @@ -27,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 9f114f2132009..93f924493c3b4 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'; @@ -565,7 +565,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) @@ -676,8 +678,7 @@ export class IndexPatternsFetcher { // // @public (undocumented) export interface ISearchOptions { - signal?: AbortSignal; - // (undocumented) + abortSignal?: AbortSignal; strategy?: string; } @@ -1063,6 +1064,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/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index 46f09a8ebb879..d3a54627b0240 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -90,6 +90,7 @@ function DevToolsWrapper({ devTools, activeDevTool, updateRoute }: DevToolsWrapp element, appBasePath: '', onAppLeave: () => undefined, + setHeaderActionMenu: () => undefined, // TODO: adapt to use Core's ScopedHistory history: {} as any, }; 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" > - { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index a0d9e3c541e47..b03b37da40908 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -62,7 +62,6 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals ); const field = new IndexPatternField( - indexPattern, { name: 'bytes', type: 'number', @@ -73,18 +72,16 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals aggregatable: true, readFromDocValues: true, }, - 'bytes', - () => {} + 'bytes' ); const props = { indexPattern, field, - getDetails: jest.fn(), + getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: true, columns: [] })), onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), - onShowDetails: jest.fn(), showDetails, selected, useShortDots, @@ -104,4 +101,9 @@ describe('discover sidebar field', function () { findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('bytes'); }); + it('should trigger getDetails', function () { + const { comp, props } = getComponent(true); + findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); + expect(props.getDetails).toHaveBeenCalledWith(props.field); + }); }); 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/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 5d7daaa7217ed..f3c4cae720193 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -31,6 +31,7 @@ import { Action } from 'src/plugins/ui_actions/public'; import { PanelOptionsMenu } from './panel_options_menu'; import { IEmbeddable } from '../../embeddables'; import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers'; +import { uiToReactComponent } from '../../../../../kibana_react/public'; export interface PanelHeaderProps { title?: string; @@ -65,7 +66,9 @@ function renderNotifications( return notifications.map((notification) => { const context = { embeddable }; - let badge = ( + let badge = notification.MenuItem ? ( + React.createElement(uiToReactComponent(notification.MenuItem)) + ) : ( | 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 aa575cd64944c..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,7 +160,7 @@ export const useGlobalFlyout = () => { Array.from(getContents()).forEach(removeContent); } }; - }, [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 8c63cc8494a8b..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,95 +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]); - 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); - } - }, - [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 1b5ca5d7f4384..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,33 +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; } - }, [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 bdea5ccf5fe26..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'; @@ -39,7 +39,7 @@ export { UseRequestResponse, sendRequest, useRequest, -} from './request/np_ready_request'; +} from './request'; export { indices } from './indices'; diff --git a/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts b/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts index a6792543cd726..b28cac1cb76f5 100644 --- a/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts +++ b/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts @@ -17,8 +17,6 @@ * under the License. */ -jest.mock('ui/new_platform'); - import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../constants'; import { diff --git a/src/plugins/es_ui_shared/public/request/index.ts b/src/plugins/es_ui_shared/public/request/index.ts index f942a9cc3932b..9e38bfe5ee16b 100644 --- a/src/plugins/es_ui_shared/public/request/index.ts +++ b/src/plugins/es_ui_shared/public/request/index.ts @@ -17,11 +17,5 @@ * under the License. */ -export { - SendRequestConfig, - SendRequestResponse, - UseRequestConfig, - UseRequestResponse, - sendRequest, - useRequest, -} from './request'; +export { SendRequestConfig, SendRequestResponse, sendRequest } from './send_request'; +export { UseRequestConfig, UseRequestResponse, useRequest } from './use_request'; diff --git a/src/plugins/es_ui_shared/public/request/np_ready_request.ts b/src/plugins/es_ui_shared/public/request/np_ready_request.ts deleted file mode 100644 index 06af698f2ce02..0000000000000 --- a/src/plugins/es_ui_shared/public/request/np_ready_request.ts +++ /dev/null @@ -1,184 +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 { useEffect, useState, useRef, useMemo } from 'react'; - -import { HttpSetup, HttpFetchQuery } from '../../../../../src/core/public'; - -export interface SendRequestConfig { - path: string; - method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'; - query?: HttpFetchQuery; - body?: any; -} - -export interface SendRequestResponse { - data: D | null; - error: E | null; -} - -export interface UseRequestConfig extends SendRequestConfig { - pollIntervalMs?: number; - initialData?: any; - deserializer?: (data: any) => any; -} - -export interface UseRequestResponse { - isInitialRequest: boolean; - isLoading: boolean; - error: E | null; - data?: D | null; - sendRequest: (...args: any[]) => Promise>; -} - -export const sendRequest = async ( - httpClient: HttpSetup, - { path, method, body, query }: SendRequestConfig -): Promise> => { - try { - const stringifiedBody = typeof body === 'string' ? body : JSON.stringify(body); - const response = await httpClient[method](path, { body: stringifiedBody, query }); - - return { - data: response.data ? response.data : response, - error: null, - }; - } catch (e) { - return { - data: null, - error: e.response && e.response.data ? e.response.data : e.body, - }; - } -}; - -export const useRequest = ( - httpClient: HttpSetup, - { - path, - method, - query, - body, - pollIntervalMs, - initialData, - deserializer = (data: any): any => data, - }: UseRequestConfig -): UseRequestResponse => { - const sendRequestRef = useRef<() => Promise>>(); - // Main states for tracking request status and data - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [data, setData] = useState(initialData); - - // Consumers can use isInitialRequest to implement a polling UX. - const [isInitialRequest, setIsInitialRequest] = useState(true); - const pollInterval = useRef(null); - const pollIntervalId = useRef(null); - - // We always want to use the most recently-set interval in scheduleRequest. - pollInterval.current = pollIntervalMs; - - // Tied to every render and bound to each request. - let isOutdatedRequest = false; - - const scheduleRequest = () => { - // Clear current interval - if (pollIntervalId.current) { - clearTimeout(pollIntervalId.current); - } - - // Set new interval - if (pollInterval.current) { - pollIntervalId.current = setTimeout( - () => (sendRequestRef.current ?? _sendRequest)(), - pollInterval.current - ); - } - }; - - const _sendRequest = async () => { - // We don't clear error or data, so it's up to the consumer to decide whether to display the - // "old" error/data or loading state when a new request is in-flight. - setIsLoading(true); - - const requestBody = { - path, - method, - query, - body, - }; - - const response = await sendRequest(httpClient, requestBody); - const { data: serializedResponseData, error: responseError } = response; - - // If an outdated request has resolved, DON'T update state, but DO allow the processData handler - // to execute side effects like update telemetry. - if (isOutdatedRequest) { - return { data: null, error: null }; - } - - setError(responseError); - - if (!responseError) { - const responseData = deserializer(serializedResponseData); - setData(responseData); - } - - setIsLoading(false); - setIsInitialRequest(false); - - // If we're on an interval, we need to schedule the next request. This also allows us to reset - // the interval if the user has manually requested the data, to avoid doubled-up requests. - scheduleRequest(); - - return { data: serializedResponseData, error: responseError }; - }; - - useEffect(() => { - sendRequestRef.current = _sendRequest; - }, [_sendRequest]); - - const stringifiedQuery = useMemo(() => JSON.stringify(query), [query]); - - useEffect(() => { - (sendRequestRef.current ?? _sendRequest)(); - // To be functionally correct we'd send a new request if the method, path, query or body changes. - // But it doesn't seem likely that the method will change and body is likely to be a new - // object even if its shape hasn't changed, so for now we're just watching the path and the query. - }, [path, stringifiedQuery]); - - useEffect(() => { - scheduleRequest(); - - // Clean up intervals and inflight requests and corresponding state changes - return () => { - isOutdatedRequest = true; - if (pollIntervalId.current) { - clearTimeout(pollIntervalId.current); - } - }; - }, [pollIntervalMs]); - - return { - isInitialRequest, - isLoading, - error, - data, - sendRequest: sendRequestRef.current ?? _sendRequest, // Gives the user the ability to manually request data - }; -}; diff --git a/src/plugins/es_ui_shared/public/request/request.test.js b/src/plugins/es_ui_shared/public/request/request.test.js deleted file mode 100644 index 190c32517eefe..0000000000000 --- a/src/plugins/es_ui_shared/public/request/request.test.js +++ /dev/null @@ -1,262 +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 { sendRequest as sendRequestUnbound, useRequest as useRequestUnbound } from './request'; - -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mount } from 'enzyme'; - -const TestHook = ({ callback }) => { - callback(); - return null; -}; - -let element; - -const testHook = (callback) => { - element = mount(); -}; - -const wait = async (wait) => new Promise((resolve) => setTimeout(resolve, wait || 1)); - -// FLAKY: -// - https://github.com/elastic/kibana/issues/42561 -// - https://github.com/elastic/kibana/issues/42562 -// - https://github.com/elastic/kibana/issues/42563 -// - https://github.com/elastic/kibana/issues/42225 -describe.skip('request lib', () => { - const successRequest = { path: '/success', method: 'post', body: {} }; - const errorRequest = { path: '/error', method: 'post', body: {} }; - const successResponse = { statusCode: 200, data: { message: 'Success message' } }; - const errorResponse = { statusCode: 400, statusText: 'Error message' }; - - let sendPost; - let sendRequest; - let useRequest; - - /** - * - * commented out due to hooks being called regardless of skip - * https://github.com/facebook/jest/issues/8379 - - beforeEach(() => { - sendPost = sinon.stub(); - sendPost.withArgs(successRequest.path, successRequest.body).returns(successResponse); - sendPost.withArgs(errorRequest.path, errorRequest.body).throws(errorResponse); - - const httpClient = { - post: (...args) => { - return sendPost(...args); - }, - }; - - sendRequest = sendRequestUnbound.bind(null, httpClient); - useRequest = useRequestUnbound.bind(null, httpClient); - }); - - */ - - describe('sendRequest function', () => { - it('uses the provided path, method, and body to send the request', async () => { - const response = await sendRequest({ ...successRequest }); - sinon.assert.calledOnce(sendPost); - expect(response).toEqual({ data: successResponse.data, error: null }); - }); - - it('surfaces errors', async () => { - try { - await sendRequest({ ...errorRequest }); - } catch (e) { - sinon.assert.calledOnce(sendPost); - expect(e).toBe(errorResponse.error); - } - }); - }); - - describe('useRequest hook', () => { - let hook; - - function initUseRequest(config) { - act(() => { - testHook(() => { - hook = useRequest(config); - }); - }); - } - - describe('parameters', () => { - describe('path, method, body', () => { - it('is used to send the request', async () => { - initUseRequest({ ...successRequest }); - await wait(50); - expect(hook.data).toBe(successResponse.data); - }); - }); - - describe('pollIntervalMs', () => { - it('sends another request after the specified time has elapsed', async () => { - initUseRequest({ ...successRequest, pollIntervalMs: 10 }); - await wait(50); - // We just care that multiple requests have been sent out. We don't check the specific - // timing because that risks introducing flakiness into the tests, and it's unlikely - // we could break the implementation by getting the exact timing wrong. - expect(sendPost.callCount).toBeGreaterThan(1); - - // We have to manually clean up or else the interval will continue to fire requests, - // interfering with other tests. - element.unmount(); - }); - }); - - describe('initialData', () => { - it('sets the initial data value', () => { - initUseRequest({ ...successRequest, initialData: 'initialData' }); - expect(hook.data).toBe('initialData'); - }); - }); - - describe('deserializer', () => { - it('is called once the request resolves', async () => { - const deserializer = sinon.stub(); - initUseRequest({ ...successRequest, deserializer }); - sinon.assert.notCalled(deserializer); - - await wait(50); - sinon.assert.calledOnce(deserializer); - sinon.assert.calledWith(deserializer, successResponse.data); - }); - - it('processes data', async () => { - initUseRequest({ ...successRequest, deserializer: () => 'intercepted' }); - await wait(50); - expect(hook.data).toBe('intercepted'); - }); - }); - }); - - describe('state', () => { - describe('isInitialRequest', () => { - it('is true for the first request and false for subsequent requests', async () => { - initUseRequest({ ...successRequest }); - expect(hook.isInitialRequest).toBe(true); - - hook.sendRequest(); - await wait(50); - expect(hook.isInitialRequest).toBe(false); - }); - }); - - describe('isLoading', () => { - it('represents in-flight request status', async () => { - initUseRequest({ ...successRequest }); - expect(hook.isLoading).toBe(true); - - await wait(50); - expect(hook.isLoading).toBe(false); - }); - }); - - describe('error', () => { - it('surfaces errors from requests', async () => { - initUseRequest({ ...errorRequest }); - await wait(50); - expect(hook.error).toBe(errorResponse); - }); - - it('persists while a request is in-flight', async () => { - initUseRequest({ ...errorRequest }); - await wait(50); - hook.sendRequest(); - expect(hook.isLoading).toBe(true); - expect(hook.error).toBe(errorResponse); - }); - - it('is null when the request is successful', async () => { - initUseRequest({ ...successRequest }); - await wait(50); - expect(hook.isLoading).toBe(false); - expect(hook.error).toBeNull(); - }); - }); - - describe('data', () => { - it('surfaces payloads from requests', async () => { - initUseRequest({ ...successRequest }); - await wait(50); - expect(hook.data).toBe(successResponse.data); - }); - - it('persists while a request is in-flight', async () => { - initUseRequest({ ...successRequest }); - await wait(50); - hook.sendRequest(); - expect(hook.isLoading).toBe(true); - expect(hook.data).toBe(successResponse.data); - }); - - it('is null when the request fails', async () => { - initUseRequest({ ...errorRequest }); - await wait(50); - expect(hook.isLoading).toBe(false); - expect(hook.data).toBeNull(); - }); - }); - }); - - describe('callbacks', () => { - describe('sendRequest', () => { - it('sends the request', () => { - initUseRequest({ ...successRequest }); - sinon.assert.calledOnce(sendPost); - hook.sendRequest(); - sinon.assert.calledTwice(sendPost); - }); - - it('resets the pollIntervalMs', async () => { - initUseRequest({ ...successRequest, pollIntervalMs: 800 }); - await wait(200); // 200ms - hook.sendRequest(); - expect(sendPost.callCount).toBe(2); - - await wait(200); // 400ms - hook.sendRequest(); - - await wait(200); // 600ms - hook.sendRequest(); - - await wait(200); // 800ms - hook.sendRequest(); - - await wait(200); // 1000ms - hook.sendRequest(); - - // If sendRequest didn't reset the interval, the interval would have triggered another - // request by now, and the callCount would be 7. - expect(sendPost.callCount).toBe(6); - - // We have to manually clean up or else the interval will continue to fire requests, - // interfering with other tests. - element.unmount(); - }); - }); - }); - }); -}); diff --git a/src/plugins/es_ui_shared/public/request/request.ts b/src/plugins/es_ui_shared/public/request/request.ts deleted file mode 100644 index fd6980367136e..0000000000000 --- a/src/plugins/es_ui_shared/public/request/request.ts +++ /dev/null @@ -1,165 +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 { useEffect, useState, useRef } from 'react'; - -export interface SendRequestConfig { - path: string; - method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'; - body?: any; -} - -export interface SendRequestResponse { - data: any; - error: Error | null; -} - -export interface UseRequestConfig extends SendRequestConfig { - pollIntervalMs?: number; - initialData?: any; - deserializer?: (data: any) => any; -} - -export interface UseRequestResponse { - isInitialRequest: boolean; - isLoading: boolean; - error: null | unknown; - data: any; - sendRequest: (...args: any[]) => Promise; -} - -export const sendRequest = async ( - httpClient: ng.IHttpService, - { path, method, body }: SendRequestConfig -): Promise => { - try { - const response = await (httpClient as any)[method](path, body); - - if (typeof response.data === 'undefined') { - throw new Error(response.statusText); - } - - return { data: response.data, error: null }; - } catch (e) { - return { - data: null, - error: e.response ? e.response : e, - }; - } -}; - -export const useRequest = ( - httpClient: ng.IHttpService, - { - path, - method, - body, - pollIntervalMs, - initialData, - deserializer = (data: any): any => data, - }: UseRequestConfig -): UseRequestResponse => { - // Main states for tracking request status and data - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [data, setData] = useState(initialData); - - // Consumers can use isInitialRequest to implement a polling UX. - const [isInitialRequest, setIsInitialRequest] = useState(true); - const pollInterval = useRef(null); - const pollIntervalId = useRef(null); - - // We always want to use the most recently-set interval in scheduleRequest. - pollInterval.current = pollIntervalMs; - - // Tied to every render and bound to each request. - let isOutdatedRequest = false; - - const scheduleRequest = () => { - // Clear current interval - if (pollIntervalId.current) { - clearTimeout(pollIntervalId.current); - } - - // Set new interval - if (pollInterval.current) { - pollIntervalId.current = setTimeout(_sendRequest, pollInterval.current); - } - }; - - const _sendRequest = async () => { - // We don't clear error or data, so it's up to the consumer to decide whether to display the - // "old" error/data or loading state when a new request is in-flight. - setIsLoading(true); - - const requestBody = { - path, - method, - body, - }; - - const response = await sendRequest(httpClient, requestBody); - const { data: serializedResponseData, error: responseError } = response; - const responseData = deserializer(serializedResponseData); - - // If an outdated request has resolved, DON'T update state, but DO allow the processData handler - // to execute side effects like update telemetry. - if (isOutdatedRequest) { - return { data: null, error: null }; - } - - setError(responseError); - setData(responseData); - setIsLoading(false); - setIsInitialRequest(false); - - // If we're on an interval, we need to schedule the next request. This also allows us to reset - // the interval if the user has manually requested the data, to avoid doubled-up requests. - scheduleRequest(); - - return { data: serializedResponseData, error: responseError }; - }; - - useEffect(() => { - _sendRequest(); - // To be functionally correct we'd send a new request if the method, path, or body changes. - // But it doesn't seem likely that the method will change and body is likely to be a new - // object even if its shape hasn't changed, so for now we're just watching the path. - }, [path]); - - useEffect(() => { - scheduleRequest(); - - // Clean up intervals and inflight requests and corresponding state changes - return () => { - isOutdatedRequest = true; - if (pollIntervalId.current) { - clearTimeout(pollIntervalId.current); - } - }; - }, [pollIntervalMs]); - - return { - isInitialRequest, - isLoading, - error, - data, - sendRequest: _sendRequest, // Gives the user the ability to manually request data - }; -}; diff --git a/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts b/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts new file mode 100644 index 0000000000000..4312780e74c0f --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts @@ -0,0 +1,83 @@ +/* + * 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 { HttpSetup, HttpFetchOptions } from '../../../../../src/core/public'; +import { + SendRequestConfig, + SendRequestResponse, + sendRequest as originalSendRequest, +} from './send_request'; + +export interface SendRequestHelpers { + getSendRequestSpy: () => sinon.SinonStub; + sendSuccessRequest: () => Promise; + getSuccessResponse: () => SendRequestResponse; + sendErrorRequest: () => Promise; + getErrorResponse: () => SendRequestResponse; +} + +const successRequest: SendRequestConfig = { method: 'post', path: '/success', body: {} }; +const successResponse = { statusCode: 200, data: { message: 'Success message' } }; + +const errorValue = { statusCode: 400, statusText: 'Error message' }; +const errorRequest: SendRequestConfig = { method: 'post', path: '/error', body: {} }; +const errorResponse = { response: { data: errorValue } }; + +export const createSendRequestHelpers = (): SendRequestHelpers => { + const sendRequestSpy = sinon.stub(); + const httpClient = { + post: (path: string, options: HttpFetchOptions) => sendRequestSpy(path, options), + }; + const sendRequest = originalSendRequest.bind(null, httpClient as HttpSetup) as ( + config: SendRequestConfig + ) => Promise>; + + // Set up successful request helpers. + sendRequestSpy + .withArgs(successRequest.path, { + body: JSON.stringify(successRequest.body), + query: undefined, + }) + .resolves(successResponse); + const sendSuccessRequest = () => sendRequest({ ...successRequest }); + const getSuccessResponse = () => ({ data: successResponse.data, error: null }); + + // Set up failed request helpers. + sendRequestSpy + .withArgs(errorRequest.path, { + body: JSON.stringify(errorRequest.body), + query: undefined, + }) + .rejects(errorResponse); + const sendErrorRequest = () => sendRequest({ ...errorRequest }); + const getErrorResponse = () => ({ + data: null, + error: errorResponse.response.data, + }); + + return { + getSendRequestSpy: () => sendRequestSpy, + sendSuccessRequest, + getSuccessResponse, + sendErrorRequest, + getErrorResponse, + }; +}; diff --git a/src/plugins/es_ui_shared/public/request/send_request.test.ts b/src/plugins/es_ui_shared/public/request/send_request.test.ts new file mode 100644 index 0000000000000..e4deaeaba817e --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/send_request.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { SendRequestHelpers, createSendRequestHelpers } from './send_request.test.helpers'; + +describe('sendRequest function', () => { + let helpers: SendRequestHelpers; + + beforeEach(() => { + helpers = createSendRequestHelpers(); + }); + + it('uses the provided path, method, and body to send the request', async () => { + const { sendSuccessRequest, getSendRequestSpy, getSuccessResponse } = helpers; + + const response = await sendSuccessRequest(); + sinon.assert.calledOnce(getSendRequestSpy()); + expect(response).toEqual(getSuccessResponse()); + }); + + it('surfaces errors', async () => { + const { sendErrorRequest, getSendRequestSpy, getErrorResponse } = helpers; + + // For some reason sinon isn't throwing an error on rejection, as an awaited Promise normally would. + const error = await sendErrorRequest(); + sinon.assert.calledOnce(getSendRequestSpy()); + expect(error).toEqual(getErrorResponse()); + }); +}); diff --git a/src/plugins/es_ui_shared/public/request/send_request.ts b/src/plugins/es_ui_shared/public/request/send_request.ts new file mode 100644 index 0000000000000..453e91570cd85 --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/send_request.ts @@ -0,0 +1,52 @@ +/* + * 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 { HttpSetup, HttpFetchQuery } from '../../../../../src/core/public'; + +export interface SendRequestConfig { + path: string; + method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'; + query?: HttpFetchQuery; + body?: any; +} + +export interface SendRequestResponse { + data: D | null; + error: E | null; +} + +export const sendRequest = async ( + httpClient: HttpSetup, + { path, method, body, query }: SendRequestConfig +): Promise> => { + try { + const stringifiedBody = typeof body === 'string' ? body : JSON.stringify(body); + const response = await httpClient[method](path, { body: stringifiedBody, query }); + + return { + data: response.data ? response.data : response, + error: null, + }; + } catch (e) { + return { + data: null, + error: e.response?.data ?? e.body, + }; + } +}; diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx new file mode 100644 index 0000000000000..0d6fd122ad22c --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx @@ -0,0 +1,184 @@ +/* + * 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 React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mount, ReactWrapper } from 'enzyme'; +import sinon from 'sinon'; + +import { HttpSetup, HttpFetchOptions } from '../../../../../src/core/public'; +import { SendRequestConfig, SendRequestResponse } from './send_request'; +import { useRequest, UseRequestResponse, UseRequestConfig } from './use_request'; + +export interface UseRequestHelpers { + advanceTime: (ms: number) => Promise; + completeRequest: () => Promise; + hookResult: UseRequestResponse; + getSendRequestSpy: () => sinon.SinonStub; + setupSuccessRequest: (overrides?: {}, requestTimings?: number[]) => void; + getSuccessResponse: () => SendRequestResponse; + setupErrorRequest: (overrides?: {}, requestTimings?: number[]) => void; + getErrorResponse: () => SendRequestResponse; + setErrorResponse: (overrides?: {}) => void; + setupErrorWithBodyRequest: (overrides?: {}) => void; + getErrorWithBodyResponse: () => SendRequestResponse; +} + +// Each request will take 1s to resolve. +export const REQUEST_TIME = 1000; + +const successRequest: SendRequestConfig = { method: 'post', path: '/success', body: {} }; +const successResponse = { statusCode: 200, data: { message: 'Success message' } }; + +const errorValue = { statusCode: 400, statusText: 'Error message' }; +const errorRequest: SendRequestConfig = { method: 'post', path: '/error', body: {} }; +const errorResponse = { response: { data: errorValue } }; + +const errorWithBodyRequest: SendRequestConfig = { + method: 'post', + path: '/errorWithBody', + body: {}, +}; +const errorWithBodyResponse = { body: errorValue }; + +export const createUseRequestHelpers = (): UseRequestHelpers => { + // The behavior we're testing involves state changes over time, so we need finer control over + // timing. + jest.useFakeTimers(); + + const flushPromiseJobQueue = async () => { + // See https://stackoverflow.com/questions/52177631/jest-timer-and-promise-dont-work-well-settimeout-and-async-function + await Promise.resolve(); + }; + + const completeRequest = async () => { + await act(async () => { + jest.runAllTimers(); + await flushPromiseJobQueue(); + }); + }; + + const advanceTime = async (ms: number) => { + await act(async () => { + jest.advanceTimersByTime(ms); + await flushPromiseJobQueue(); + }); + }; + + let element: ReactWrapper; + // We'll use this object to observe the state of the hook and access its callback(s). + const hookResult = {} as UseRequestResponse; + const sendRequestSpy = sinon.stub(); + + const setupUseRequest = (config: UseRequestConfig, requestTimings?: number[]) => { + let requestCount = 0; + + const httpClient = { + post: (path: string, options: HttpFetchOptions) => { + return new Promise((resolve, reject) => { + // Increase the time it takes to resolve a request so we have time to inspect the hook + // as it goes through various states. + setTimeout(() => { + try { + resolve(sendRequestSpy(path, options)); + } catch (e) { + reject(e); + } + }, (requestTimings && requestTimings[requestCount++]) || REQUEST_TIME); + }); + }, + }; + + const TestComponent = ({ requestConfig }: { requestConfig: UseRequestConfig }) => { + const { isInitialRequest, isLoading, error, data, sendRequest } = useRequest( + httpClient as HttpSetup, + requestConfig + ); + + hookResult.isInitialRequest = isInitialRequest; + hookResult.isLoading = isLoading; + hookResult.error = error; + hookResult.data = data; + hookResult.sendRequest = sendRequest; + + return null; + }; + + act(() => { + element = mount(); + }); + }; + + // Set up successful request helpers. + sendRequestSpy + .withArgs(successRequest.path, { + body: JSON.stringify(successRequest.body), + query: undefined, + }) + .resolves(successResponse); + const setupSuccessRequest = (overrides = {}, requestTimings?: number[]) => + setupUseRequest({ ...successRequest, ...overrides }, requestTimings); + const getSuccessResponse = () => ({ data: successResponse.data, error: null }); + + // Set up failed request helpers. + sendRequestSpy + .withArgs(errorRequest.path, { + body: JSON.stringify(errorRequest.body), + query: undefined, + }) + .rejects(errorResponse); + const setupErrorRequest = (overrides = {}, requestTimings?: number[]) => + setupUseRequest({ ...errorRequest, ...overrides }, requestTimings); + const getErrorResponse = () => ({ + data: null, + error: errorResponse.response.data, + }); + // We'll use this to change a success response to an error response, to test how the state changes. + const setErrorResponse = (overrides = {}) => { + element.setProps({ requestConfig: { ...errorRequest, ...overrides } }); + }; + + // Set up failed request helpers with the alternative error shape. + sendRequestSpy + .withArgs(errorWithBodyRequest.path, { + body: JSON.stringify(errorWithBodyRequest.body), + query: undefined, + }) + .rejects(errorWithBodyResponse); + const setupErrorWithBodyRequest = (overrides = {}) => + setupUseRequest({ ...errorWithBodyRequest, ...overrides }); + const getErrorWithBodyResponse = () => ({ + data: null, + error: errorWithBodyResponse.body, + }); + + return { + advanceTime, + completeRequest, + hookResult, + getSendRequestSpy: () => sendRequestSpy, + setupSuccessRequest, + getSuccessResponse, + setupErrorRequest, + getErrorResponse, + setErrorResponse, + setupErrorWithBodyRequest, + getErrorWithBodyResponse, + }; +}; diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.ts b/src/plugins/es_ui_shared/public/request/use_request.test.ts new file mode 100644 index 0000000000000..f7902218d9314 --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/use_request.test.ts @@ -0,0 +1,353 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import sinon from 'sinon'; + +import { + UseRequestHelpers, + REQUEST_TIME, + createUseRequestHelpers, +} from './use_request.test.helpers'; + +describe('useRequest hook', () => { + let helpers: UseRequestHelpers; + + beforeEach(() => { + helpers = createUseRequestHelpers(); + }); + + describe('parameters', () => { + describe('path, method, body', () => { + it('is used to send the request', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; + setupSuccessRequest(); + await completeRequest(); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + }); + + describe('pollIntervalMs', () => { + it('sends another request after the specified time has elapsed', async () => { + const { setupSuccessRequest, advanceTime, getSendRequestSpy } = helpers; + setupSuccessRequest({ pollIntervalMs: REQUEST_TIME }); + + await advanceTime(REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(1); + + // We need to advance (1) the pollIntervalMs and (2) the request time. + await advanceTime(REQUEST_TIME * 2); + expect(getSendRequestSpy().callCount).toBe(2); + + // We need to advance (1) the pollIntervalMs and (2) the request time. + await advanceTime(REQUEST_TIME * 2); + expect(getSendRequestSpy().callCount).toBe(3); + }); + }); + + describe('initialData', () => { + it('sets the initial data value', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; + setupSuccessRequest({ initialData: 'initialData' }); + expect(hookResult.data).toBe('initialData'); + + // The initial data value will be overwritten once the request resolves. + await completeRequest(); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + }); + + describe('deserializer', () => { + it('is called with the response once the request resolves', async () => { + const { setupSuccessRequest, completeRequest, getSuccessResponse } = helpers; + + const deserializer = sinon.stub(); + setupSuccessRequest({ deserializer }); + sinon.assert.notCalled(deserializer); + await completeRequest(); + + sinon.assert.calledOnce(deserializer); + sinon.assert.calledWith(deserializer, getSuccessResponse().data); + }); + + it('provides the data return value', async () => { + const { setupSuccessRequest, completeRequest, hookResult } = helpers; + setupSuccessRequest({ deserializer: () => 'intercepted' }); + await completeRequest(); + expect(hookResult.data).toBe('intercepted'); + }); + }); + }); + + describe('state', () => { + describe('isInitialRequest', () => { + it('is true for the first request and false for subsequent requests', async () => { + const { setupSuccessRequest, completeRequest, hookResult } = helpers; + setupSuccessRequest(); + expect(hookResult.isInitialRequest).toBe(true); + + hookResult.sendRequest(); + await completeRequest(); + expect(hookResult.isInitialRequest).toBe(false); + }); + }); + + describe('isLoading', () => { + it('represents in-flight request status', async () => { + const { setupSuccessRequest, completeRequest, hookResult } = helpers; + setupSuccessRequest(); + expect(hookResult.isLoading).toBe(true); + + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + }); + }); + + describe('error', () => { + it('surfaces errors from requests', async () => { + const { setupErrorRequest, completeRequest, hookResult, getErrorResponse } = helpers; + setupErrorRequest(); + await completeRequest(); + expect(hookResult.error).toBe(getErrorResponse().error); + }); + + it('surfaces body-shaped errors from requests', async () => { + const { + setupErrorWithBodyRequest, + completeRequest, + hookResult, + getErrorWithBodyResponse, + } = helpers; + + setupErrorWithBodyRequest(); + await completeRequest(); + expect(hookResult.error).toBe(getErrorWithBodyResponse().error); + }); + + it('persists while a request is in-flight', async () => { + const { setupErrorRequest, completeRequest, hookResult, getErrorResponse } = helpers; + setupErrorRequest(); + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.error).toBe(getErrorResponse().error); + + act(() => { + hookResult.sendRequest(); + }); + expect(hookResult.isLoading).toBe(true); + expect(hookResult.error).toBe(getErrorResponse().error); + }); + + it('is null when the request is successful', async () => { + const { setupSuccessRequest, completeRequest, hookResult } = helpers; + setupSuccessRequest(); + expect(hookResult.error).toBeNull(); + + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.error).toBeNull(); + }); + }); + + describe('data', () => { + it('surfaces payloads from requests', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; + setupSuccessRequest(); + expect(hookResult.data).toBeUndefined(); + + await completeRequest(); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + + it('persists while a request is in-flight', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; + setupSuccessRequest(); + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.data).toBe(getSuccessResponse().data); + + act(() => { + hookResult.sendRequest(); + }); + expect(hookResult.isLoading).toBe(true); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + + it('persists from last successful request when the next request fails', async () => { + const { + setupSuccessRequest, + completeRequest, + hookResult, + getErrorResponse, + setErrorResponse, + getSuccessResponse, + } = helpers; + + setupSuccessRequest(); + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.error).toBeNull(); + expect(hookResult.data).toBe(getSuccessResponse().data); + + setErrorResponse(); + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.error).toBe(getErrorResponse().error); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + }); + }); + + describe('callbacks', () => { + describe('sendRequest', () => { + it('sends the request', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSendRequestSpy } = helpers; + setupSuccessRequest(); + + await completeRequest(); + expect(getSendRequestSpy().callCount).toBe(1); + + await act(async () => { + hookResult.sendRequest(); + await completeRequest(); + }); + expect(getSendRequestSpy().callCount).toBe(2); + }); + + it('resets the pollIntervalMs', async () => { + const { setupSuccessRequest, advanceTime, hookResult, getSendRequestSpy } = helpers; + const DOUBLE_REQUEST_TIME = REQUEST_TIME * 2; + setupSuccessRequest({ pollIntervalMs: DOUBLE_REQUEST_TIME }); + + // The initial request resolves, and then we'll immediately send a new one manually... + await advanceTime(REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(1); + act(() => { + hookResult.sendRequest(); + }); + + // The manual request resolves, and we'll send yet another one... + await advanceTime(REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(2); + act(() => { + hookResult.sendRequest(); + }); + + // At this point, we've moved forward 3s. The poll is set at 2s. If sendRequest didn't + // reset the poll, the request call count would be 4, not 3. + await advanceTime(REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(3); + }); + }); + }); + + describe('request behavior', () => { + it('outdated responses are ignored by poll requests', async () => { + const { + setupSuccessRequest, + setErrorResponse, + completeRequest, + hookResult, + getErrorResponse, + getSendRequestSpy, + } = helpers; + const DOUBLE_REQUEST_TIME = REQUEST_TIME * 2; + // Send initial request, which will have a longer round-trip time. + setupSuccessRequest({}, [DOUBLE_REQUEST_TIME]); + + // Send a new request, which will have a shorter round-trip time. + setErrorResponse(); + + // Complete both requests. + await completeRequest(); + + // Two requests were sent... + expect(getSendRequestSpy().callCount).toBe(2); + // ...but the error response is the one that takes precedence because it was *sent* more + // recently, despite the success response *returning* more recently. + expect(hookResult.error).toBe(getErrorResponse().error); + expect(hookResult.data).toBeUndefined(); + }); + + it(`outdated responses are ignored if there's a more recently-sent manual request`, async () => { + const { setupSuccessRequest, advanceTime, hookResult, getSendRequestSpy } = helpers; + + const HALF_REQUEST_TIME = REQUEST_TIME * 0.5; + setupSuccessRequest({ pollIntervalMs: REQUEST_TIME }); + + // Before the original request resolves, we make a manual sendRequest call. + await advanceTime(HALF_REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(0); + act(() => { + hookResult.sendRequest(); + }); + + // The original quest resolves but it's been marked as outdated by the the manual sendRequest + // call "interrupts", so data is left undefined. + await advanceTime(HALF_REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(1); + expect(hookResult.data).toBeUndefined(); + }); + + it(`changing pollIntervalMs doesn't trigger a new request`, async () => { + const { setupErrorRequest, setErrorResponse, completeRequest, getSendRequestSpy } = helpers; + const DOUBLE_REQUEST_TIME = REQUEST_TIME * 2; + // Send initial request. + setupErrorRequest({ pollIntervalMs: REQUEST_TIME }); + + // Setting a new poll will schedule a second request, but not send one immediately. + setErrorResponse({ pollIntervalMs: DOUBLE_REQUEST_TIME }); + + // Complete initial request. + await completeRequest(); + + // Complete scheduled poll request. + await completeRequest(); + expect(getSendRequestSpy().callCount).toBe(2); + }); + + it('when the path changes after a request is scheduled, the scheduled request is sent with that path', async () => { + const { + setupSuccessRequest, + completeRequest, + hookResult, + getErrorResponse, + setErrorResponse, + getSendRequestSpy, + } = helpers; + const DOUBLE_REQUEST_TIME = REQUEST_TIME * 2; + + // Sned first request and schedule a request, both with the success path. + setupSuccessRequest({ pollIntervalMs: DOUBLE_REQUEST_TIME }); + + // Change the path to the error path, sending a second request. pollIntervalMs is the same + // so the originally scheduled poll remains cheduled. + setErrorResponse({ pollIntervalMs: DOUBLE_REQUEST_TIME }); + + // Complete the initial request, the requests by the path change, and the scheduled poll request. + await completeRequest(); + await completeRequest(); + + // If the scheduled poll request was sent to the success path, we wouldn't have an error result. + // But we do, because it was sent to the error path. + expect(getSendRequestSpy().callCount).toBe(3); + expect(hookResult.error).toBe(getErrorResponse().error); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts new file mode 100644 index 0000000000000..481843bf40e88 --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/use_request.ts @@ -0,0 +1,161 @@ +/* + * 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 { useEffect, useCallback, useState, useRef, useMemo } from 'react'; + +import { HttpSetup } from '../../../../../src/core/public'; +import { + sendRequest as sendStatelessRequest, + SendRequestConfig, + SendRequestResponse, +} from './send_request'; + +export interface UseRequestConfig extends SendRequestConfig { + pollIntervalMs?: number; + initialData?: any; + deserializer?: (data: any) => any; +} + +export interface UseRequestResponse { + isInitialRequest: boolean; + isLoading: boolean; + error: E | null; + data?: D | null; + sendRequest: () => Promise>; +} + +export const useRequest = ( + httpClient: HttpSetup, + { path, method, query, body, pollIntervalMs, initialData, deserializer }: UseRequestConfig +): UseRequestResponse => { + const isMounted = useRef(false); + + // Main states for tracking request status and data + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [data, setData] = useState(initialData); + + // Consumers can use isInitialRequest to implement a polling UX. + const requestCountRef = useRef(0); + const isInitialRequest = requestCountRef.current === 0; + const pollIntervalIdRef = useRef(null); + + const clearPollInterval = useCallback(() => { + if (pollIntervalIdRef.current) { + clearTimeout(pollIntervalIdRef.current); + pollIntervalIdRef.current = null; + } + }, []); + + // Convert our object to string to be able to compare them in our useMemo, + // allowing the consumer to freely passed new objects to the hook on each + // render without requiring them to be memoized. + const queryStringified = query ? JSON.stringify(query) : undefined; + const bodyStringified = body ? JSON.stringify(body) : undefined; + + const requestBody = useMemo(() => { + return { + path, + method, + query: queryStringified ? query : undefined, + body: bodyStringified ? body : undefined, + }; + // queryStringified and bodyStringified stand in for query and body as dependencies. + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [path, method, queryStringified, bodyStringified]); + + const sendRequest = useCallback(async () => { + // If we're on an interval, this allows us to reset it if the user has manually requested the + // data, to avoid doubled-up requests. + clearPollInterval(); + + const requestId = ++requestCountRef.current; + + // We don't clear error or data, so it's up to the consumer to decide whether to display the + // "old" error/data or loading state when a new request is in-flight. + setIsLoading(true); + + const response = await sendStatelessRequest(httpClient, requestBody); + const { data: serializedResponseData, error: responseError } = response; + + const isOutdatedRequest = requestId !== requestCountRef.current; + const isUnmounted = isMounted.current === false; + + // Ignore outdated or irrelevant data. + if (isOutdatedRequest || isUnmounted) { + return { data: null, error: null }; + } + + setError(responseError); + // If there's an error, keep the data from the last request in case it's still useful to the user. + if (!responseError) { + const responseData = deserializer + ? deserializer(serializedResponseData) + : serializedResponseData; + setData(responseData); + } + // Setting isLoading to false also acts as a signal for scheduling the next poll request. + setIsLoading(false); + + return { data: serializedResponseData, error: responseError }; + }, [requestBody, httpClient, deserializer, clearPollInterval]); + + const scheduleRequest = useCallback(() => { + // If there's a scheduled poll request, this new one will supersede it. + clearPollInterval(); + + if (pollIntervalMs) { + pollIntervalIdRef.current = setTimeout(sendRequest, pollIntervalMs); + } + }, [pollIntervalMs, sendRequest, clearPollInterval]); + + // Send the request on component mount and whenever the dependencies of sendRequest() change. + useEffect(() => { + sendRequest(); + }, [sendRequest]); + + // Schedule the next poll request when the previous one completes. + useEffect(() => { + // When a request completes, attempt to schedule the next one. Note that we aren't re-scheduling + // a request whenever sendRequest's dependencies change. isLoading isn't set to false until the + // initial request has completed, so we won't schedule a request on mount. + if (!isLoading) { + scheduleRequest(); + } + }, [isLoading, scheduleRequest]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + + // Clean up on unmount. + clearPollInterval(); + }; + }, [clearPollInterval]); + + return { + isInitialRequest, + isLoading, + error, + data, + sendRequest, // Gives the user the ability to manually request data + }; +}; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx index 9fb804eb7fafa..b2f1a70341315 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx @@ -74,7 +74,7 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => }; const onSearchComboChange = (value: string) => { - if (value) { + if (value !== undefined) { field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM); } }; 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 e02c9b6c18615..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,15 +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); }, - [field.onChange] + [onFieldChange] ); return ( diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx index b3a15fea8b187..287ac56243446 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx @@ -21,6 +21,7 @@ import React, { ReactNode } from 'react'; import { EuiForm } from '@elastic/eui'; import { FormProvider } from '../form_context'; +import { FormDataContextProvider } from '../form_data_context'; import { FormHook } from '../types'; interface Props { @@ -30,8 +31,14 @@ interface Props { [key: string]: any; } -export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => ( - - - -); +export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => { + const { getFormData, __getFormData$ } = form; + + return ( + + + + + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx index 25448dff18e8a..d9095944eaa33 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx @@ -75,16 +75,7 @@ describe('', () => { setInputValue('lastNameField', 'updated value'); }); - /** - * The children will be rendered three times: - * - Twice for each input value that has changed - * - once because after updating both fields, the **form** isValid state changes (from "undefined" to "true") - * causing a new "form" object to be returned and thus a re-render. - * - * When the form object will be memoized (in a future PR), te bellow call count should only be 2 as listening - * to form data changes should not receive updates when the "isValid" state of the form changes. - */ - expect(onFormData.mock.calls.length).toBe(3); + expect(onFormData).toBeCalledTimes(2); const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< OnUpdateHandler @@ -130,7 +121,7 @@ describe('', () => { find, } = setup() as TestBed; - expect(onFormData.mock.calls.length).toBe(0); // Not present in the DOM yet + expect(onFormData).toBeCalledTimes(0); // Not present in the DOM yet // Make some changes to the form fields await act(async () => { @@ -188,7 +179,7 @@ describe('', () => { setInputValue('lastNameField', 'updated value'); }); - expect(onFormData.mock.calls.length).toBe(0); + expect(onFormData).toBeCalledTimes(0); }); test('props.pathsToWatch (Array): should not re-render the children when the field that changed is not in the watch list', async () => { @@ -228,14 +219,14 @@ describe('', () => { }); // No re-render - expect(onFormData.mock.calls.length).toBe(0); + expect(onFormData).toBeCalledTimes(0); // Make some changes to fields in the watch list await act(async () => { setInputValue('nameField', 'updated value'); }); - expect(onFormData.mock.calls.length).toBe(1); + expect(onFormData).toBeCalledTimes(1); onFormData.mockReset(); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index 3630b902f0564..ac141baf8fc71 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -17,10 +17,10 @@ * under the License. */ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React from 'react'; import { FormData } from '../types'; -import { useFormContext } from '../form_context'; +import { useFormData } from '../hooks'; interface Props { children: (formData: FormData) => JSX.Element | null; @@ -28,46 +28,9 @@ interface Props { } export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => { - const form = useFormContext(); - const { subscribe } = form; - const previousRawData = useRef(form.__getFormData$().value); - const isMounted = useRef(false); - const [formData, setFormData] = useState(previousRawData.current); + const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch }); - const onFormData = useCallback( - ({ data: { raw } }) => { - // To avoid re-rendering the children for updates on the form data - // that we are **not** interested in, we can specify one or multiple path(s) - // to watch. - if (pathsToWatch) { - const valuesToWatchArray = Array.isArray(pathsToWatch) - ? (pathsToWatch as string[]) - : ([pathsToWatch] as string[]); - - if (valuesToWatchArray.some((value) => previousRawData.current[value] !== raw[value])) { - previousRawData.current = raw; - setFormData(raw); - } - } else { - setFormData(raw); - } - }, - [pathsToWatch] - ); - - useEffect(() => { - const subscription = subscribe(onFormData); - return subscription.unsubscribe; - }, [subscribe, onFormData]); - - useEffect(() => { - isMounted.current = true; - return () => { - isMounted.current = false; - }; - }, []); - - if (!isMounted.current && Object.keys(formData).length === 0) { + if (!isReady) { // No field has mounted yet, don't render anything return null; } 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/form_data_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx new file mode 100644 index 0000000000000..0e6a75e9c5065 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx @@ -0,0 +1,50 @@ +/* + * 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 React, { createContext, useContext, useMemo } from 'react'; + +import { FormData, FormHook } from './types'; +import { Subject } from './lib'; + +export interface Context { + getFormData$: () => Subject; + getFormData: FormHook['getFormData']; +} + +const FormDataContext = createContext | undefined>(undefined); + +interface Props extends Context { + children: React.ReactNode; +} + +export const FormDataContextProvider = ({ children, getFormData$, getFormData }: Props) => { + const value = useMemo( + () => ({ + getFormData, + getFormData$, + }), + [getFormData, getFormData$] + ); + + return {children}; +}; + +export function useFormDataContext() { + return useContext | undefined>(FormDataContext); +} diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts index 6a04a592227f9..45c11dd6272e4 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts @@ -19,3 +19,4 @@ export { useField } from './use_field'; export { useForm } from './use_form'; +export { useFormData } from './use_form_data'; 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 9d22e4eb2ee5e..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, @@ -254,6 +249,8 @@ export const useField = ( validationErrors.push({ ...validationResult, + // See comment below that explains why we add "__isBlocking__". + __isBlocking__: validationResult.__isBlocking__ ?? validation.isBlocking, validationType: validationType || VALIDATION_TYPES.FIELD, }); @@ -306,6 +303,11 @@ export const useField = ( validationErrors.push({ ...(validationResult as ValidationError), + // We add an "__isBlocking__" property to know if this error is a blocker or no. + // Most validation errors are blockers but in some cases a validation is more a warning than an error + // like with the ComboBox items when they are added. + __isBlocking__: + (validationResult as ValidationError).__isBlocking__ ?? validation.isBlocking, validationType: validationType || VALIDATION_TYPES.FIELD, }); @@ -394,7 +396,13 @@ export const useField = ( ); const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { - setErrors(_errors.map((error) => ({ validationType: VALIDATION_TYPES.FIELD, ...error }))); + setErrors( + _errors.map((error) => ({ + validationType: VALIDATION_TYPES.FIELD, + __isBlocking__: true, + ...error, + })) + ); }, []); /** @@ -455,6 +463,7 @@ export const useField = ( setErrors([]); if (resetValue) { + hasBeenReset.current = true; const newValue = deserializeValue(updatedDefaultValue ?? defaultValue); setValue(newValue); return newValue; @@ -463,7 +472,8 @@ export const useField = ( [setValue, deserializeValue, defaultValue] ); - const isValid = errors.length === 0; + // Don't take into account non blocker validation. Some are just warning (like trying to add a wrong ComboBox item) + const isValid = errors.filter((e) => e.__isBlocking__ !== false).length === 0; const field = useMemo>(() => { return { @@ -525,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 007e492243bac..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 { @@ -39,7 +45,7 @@ const onFormHook = (_form: FormHook) => { formHook = _form; }; -describe('use_form() hook', () => { +describe('useForm() hook', () => { beforeEach(() => { formHook = null; }); @@ -441,5 +447,57 @@ describe('use_form() 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/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 35bac5b9a58c6..7b72a9eeacf7b 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -240,6 +240,12 @@ export function useForm( if (!field.isValidated) { setIsValid(undefined); + + // When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**. + // If a field is added and it is not validated it means that we have swapped fields and added new ones: + // --> we have basically have a new form in front of us. + // For that reason we make sure that the "isSubmitted" state is false. + setIsSubmitted(false); } }, [updateFormDataAt] @@ -389,6 +395,7 @@ export function useForm( isValid, id, submit: submitForm, + validate: validateAllFields, subscribe, setFieldValue, setFieldErrors, @@ -428,6 +435,7 @@ export function useForm( addField, removeField, validateFields, + validateAllFields, ]); useEffect(() => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx new file mode 100644 index 0000000000000..0fb65daecf2f4 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx @@ -0,0 +1,234 @@ +/* + * 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 React, { useEffect } from 'react'; +import { act } from 'react-dom/test-utils'; + +import { registerTestBed, TestBed } from '../shared_imports'; +import { Form, UseField } from '../components'; +import { useForm } from './use_form'; +import { useFormData, HookReturn } from './use_form_data'; + +interface Props { + onChange(data: HookReturn): void; + watch?: string | string[]; +} + +describe('useFormData() hook', () => { + const HookListenerComp = React.memo(({ onChange, watch }: Props) => { + const hookValue = useFormData({ watch }); + + useEffect(() => { + onChange(hookValue); + }, [hookValue, onChange]); + + return null; + }); + + describe('form data updates', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
    + + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ onChange: onChangeSpy }) as TestBed; + }); + + test('should return the form data', () => { + // Called twice: + // once when the hook is called and once when the fields have mounted and updated the form data + expect(onChangeSpy).toBeCalledTimes(2); + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleInitialValue' }); + }); + + test('should listen to field changes', async () => { + const { + form: { setInputValue }, + } = testBed; + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + expect(onChangeSpy).toBeCalledTimes(3); + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleChanged' }); + }); + }); + + describe('format form data', () => { + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
    + + + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + setup({ onChange: onChangeSpy }); + }); + + test('should expose a handler to build the form data', () => { + const { 1: format } = getLastMockValue(); + expect(format()).toEqual({ + user: { + firstName: 'John', + lastName: 'Snow', + }, + }); + }); + }); + + describe('options', () => { + describe('watch', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
    + + + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ watch: 'title', onChange: onChangeSpy }) as TestBed; + }); + + test('should not listen to changes on fields we are not interested in', async () => { + const { + form: { setInputValue }, + } = testBed; + + await act(async () => { + // Changing a field we are **not** interested in + setInputValue('subTitleField', 'subTitleChanged'); + // Changing a field we **are** interested in + setInputValue('titleField', 'titleChanged'); + }); + + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleChanged', subTitle: 'subTitleInitialValue' }); + }); + }); + + describe('form', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = ({ onChange }: Props) => { + const { form } = useForm(); + const hookValue = useFormData({ form }); + + useEffect(() => { + onChange(hookValue); + }, [hookValue, onChange]); + + return ( +
    + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ onChange: onChangeSpy }) as TestBed; + }); + + test('should allow a form to be provided when the hook is called outside of the FormDataContext', async () => { + const { + form: { setInputValue }, + } = testBed; + + const [initialData] = getLastMockValue(); + expect(initialData).toEqual({ title: 'titleInitialValue' }); + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + const [updatedData] = getLastMockValue(); + expect(updatedData).toEqual({ title: 'titleChanged' }); + }); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts new file mode 100644 index 0000000000000..fb4a0984438ad --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -0,0 +1,91 @@ +/* + * 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 { useState, useEffect, useRef, useCallback } from 'react'; + +import { FormData, FormHook } from '../types'; +import { useFormDataContext, Context } from '../form_data_context'; + +interface Options { + watch?: string | string[]; + form?: FormHook; +} + +export type HookReturn = [FormData, () => T, boolean]; + +export const useFormData = (options: Options = {}): HookReturn => { + const { watch, form } = options; + const ctx = useFormDataContext(); + + let getFormData: Context['getFormData']; + let getFormData$: Context['getFormData$']; + + if (form !== undefined) { + getFormData = form.getFormData; + getFormData$ = form.__getFormData$; + } else if (ctx !== undefined) { + ({ getFormData, getFormData$ } = ctx); + } else { + throw new Error( + 'useFormData() must be used within a or you need to pass FormHook object in the options.' + ); + } + + const initialValue = getFormData$().value; + + const previousRawData = useRef(initialValue); + const isMounted = useRef(false); + const [formData, setFormData] = useState(previousRawData.current); + + const formatFormData = useCallback(() => { + return getFormData({ unflatten: true }); + }, [getFormData]); + + useEffect(() => { + const subscription = getFormData$().subscribe((raw) => { + if (watch) { + const valuesToWatchArray = Array.isArray(watch) + ? (watch as string[]) + : ([watch] as string[]); + + if (valuesToWatchArray.some((value) => previousRawData.current[value] !== raw[value])) { + previousRawData.current = raw; + // Only update the state if one of the field we watch has changed. + setFormData(raw); + } + } else { + setFormData(raw); + } + }); + return subscription.unsubscribe; + }, [getFormData$, watch]); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + if (!isMounted.current && Object.keys(formData).length === 0) { + // No field has mounted yet + return [formData, formatFormData, false]; + } + + return [formData, formatFormData, true]; +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts index 3079814c9ad14..8d6b57fbeb315 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts @@ -19,7 +19,7 @@ // Only export the useForm hook. The "useField" hook is for internal use // as the consumer of the library must use the component -export { useForm } from './hooks'; +export { useForm, useFormData } from './hooks'; export { getFieldValidityAndErrorMessage } from './helpers'; export * from './form_context'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index dc495f6eb56b4..4b343ec5e9f2e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -30,6 +30,7 @@ export interface FormHook { readonly isValid: boolean | undefined; readonly id: string; submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; + validate: () => Promise; subscribe: (handler: OnUpdateHandler) => Subscription; setFieldValue: (fieldName: string, value: FieldValue) => void; setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; @@ -147,6 +148,7 @@ export interface ValidationError { message: string; code?: T; validationType?: string; + __isBlocking__?: boolean; [key: string]: any; } @@ -185,5 +187,11 @@ type FieldValue = unknown; export interface ValidationConfig { validator: ValidationFunc; type?: string; + /** + * By default all validation are blockers, which means that if they fail, the field is invalid. + * In some cases, like when trying to add an item to the ComboBox, if the item is not valid we want + * to show a validation error. But this validation is **not** blocking. Simply, the item has not been added. + */ + isBlocking?: boolean; exitOnFail?: boolean; } 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/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 8139bc6d38ab1..f55462bdc9e28 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -201,26 +201,18 @@ class TutorialUi extends React.Component { * @return {Promise} */ fetchEsHitsStatus = async (esHitsCheckConfig) => { - const searchHeader = JSON.stringify({ index: esHitsCheckConfig.index }); - const searchBody = JSON.stringify({ query: esHitsCheckConfig.query, size: 1 }); - const response = await fetch(this.props.addBasePath('/elasticsearch/_msearch'), { - method: 'post', - body: `${searchHeader}\n${searchBody}\n`, - headers: { - accept: 'application/json', - 'content-type': 'application/x-ndjson', - 'kbn-xsrf': 'kibana', - }, - credentials: 'same-origin', - }); - - if (response.status > 300) { + const { http } = getServices(); + try { + const response = await http.post('/api/home/hits_status', { + body: JSON.stringify({ + index: esHitsCheckConfig.index, + query: esHitsCheckConfig.query, + }), + }); + return response.count > 0 ? StatusCheckStates.HAS_DATA : StatusCheckStates.NO_DATA; + } catch (e) { return StatusCheckStates.ERROR; } - - const results = await response.json(); - const numHits = _.get(results, 'responses.[0].hits.hits.length', 0); - return numHits === 0 ? StatusCheckStates.NO_DATA : StatusCheckStates.HAS_DATA; }; renderInstructionSetsToggle = () => { diff --git a/src/plugins/home/server/plugin.test.ts b/src/plugins/home/server/plugin.test.ts index 33d907315e512..58103430b4d7c 100644 --- a/src/plugins/home/server/plugin.test.ts +++ b/src/plugins/home/server/plugin.test.ts @@ -19,10 +19,7 @@ import { registryForTutorialsMock, registryForSampleDataMock } from './plugin.test.mocks'; import { HomeServerPlugin } from './plugin'; -import { coreMock } from '../../../core/server/mocks'; -import { CoreSetup } from '../../../core/server'; - -type MockedKeys = { [P in keyof T]: jest.Mocked }; +import { coreMock, httpServiceMock } from '../../../core/server/mocks'; describe('HomeServerPlugin', () => { beforeEach(() => { @@ -33,8 +30,16 @@ describe('HomeServerPlugin', () => { }); describe('setup', () => { - const mockCoreSetup: MockedKeys = coreMock.createSetup(); - const initContext = coreMock.createPluginInitializerContext(); + let mockCoreSetup: ReturnType; + let initContext: ReturnType; + let routerMock: ReturnType; + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + routerMock = httpServiceMock.createRouter(); + mockCoreSetup.http.createRouter.mockReturnValue(routerMock); + initContext = coreMock.createPluginInitializerContext(); + }); test('wires up tutorials provider service and returns registerTutorial and addScopedTutorialContextFactory', () => { const setup = new HomeServerPlugin(initContext).setup(mockCoreSetup, {}); @@ -52,6 +57,18 @@ describe('HomeServerPlugin', () => { expect(setup.sampleData).toHaveProperty('addAppLinksToSampleDataset'); expect(setup.sampleData).toHaveProperty('replacePanelInSampleDatasetDashboard'); }); + + test('registers the `/api/home/hits_status` route', () => { + new HomeServerPlugin(initContext).setup(mockCoreSetup, {}); + + expect(routerMock.post).toHaveBeenCalledTimes(1); + expect(routerMock.post).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/home/hits_status', + }), + expect.any(Function) + ); + }); }); describe('start', () => { diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index 1050c19362ae1..a2f8eec686b21 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -28,6 +28,7 @@ import { import { UsageCollectionSetup } from '../../usage_collection/server'; import { capabilitiesProvider } from './capabilities_provider'; import { sampleDataTelemetry } from './saved_objects'; +import { registerRoutes } from './routes'; interface HomeServerPluginSetupDependencies { usageCollection?: UsageCollectionSetup; @@ -41,6 +42,10 @@ export class HomeServerPlugin implements Plugin { + router.post( + { + path: '/api/home/hits_status', + validate: { + body: schema.object({ + index: schema.string(), + query: schema.recordOf(schema.string(), schema.any()), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { index, query } = req.body; + const client = context.core.elasticsearch.client; + + try { + const { body } = await client.asCurrentUser.search({ + index, + size: 1, + body: { + query, + }, + }); + const count = body.hits.hits.length; + + return res.ok({ + body: { + count, + }, + }); + } catch (e) { + return res.badRequest({ + body: e, + }); + } + }) + ); +}; diff --git a/src/plugins/home/server/routes/index.ts b/src/plugins/home/server/routes/index.ts new file mode 100644 index 0000000000000..bf492051cdedf --- /dev/null +++ b/src/plugins/home/server/routes/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { IRouter } from 'src/core/server'; +import { registerHitsStatusRoute } from './fetch_es_hits_status'; + +export const registerRoutes = (router: IRouter) => { + registerHitsStatusRoute(router); +}; 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/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 987e8f0dae3a0..a0eecef66ff93 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -121,7 +121,7 @@ export const EditIndexPattern = withRouter( const refreshFields = () => { overlays.openConfirm(confirmMessage, confirmModalOptionsRefresh).then(async (isConfirmed) => { if (isConfirmed) { - await indexPattern.init(true); + await indexPattern.refreshFields(); setFields(indexPattern.getNonScriptedFields()); } }); 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/scripted_fields_table/scripted_field_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx index 80132167b7f58..ed50317aed6a0 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx @@ -46,14 +46,6 @@ jest.mock('./components/table', () => ({ }, })); -jest.mock('ui/documentation_links', () => ({ - documentationLinks: { - scriptedFields: { - painless: 'painlessDocs', - }, - }, -})); - const helpers = { redirectToRoute: () => {}, getRouteHref: () => '#', 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 (
    - + {formatter(value).formatted} +
    + ); +} + +describe('useFormatter', () => { + const timeSeries = ([ + { + data: [ + { x: 1, y: toMicroseconds(11, 'minutes') }, + { x: 2, y: toMicroseconds(1, 'minutes') }, + { x: 3, y: toMicroseconds(60, 'seconds') }, + ], + }, + { + data: [ + { x: 1, y: toMicroseconds(120, 'seconds') }, + { x: 2, y: toMicroseconds(1, 'minutes') }, + { x: 3, y: toMicroseconds(60, 'seconds') }, + ], + }, + { + data: [ + { x: 1, y: toMicroseconds(60, 'seconds') }, + { x: 2, y: toMicroseconds(5, 'minutes') }, + { x: 3, y: toMicroseconds(100, 'seconds') }, + ], + }, + ] as unknown) as TimeSeries[]; + it('returns new formatter when disabled series state changes', () => { + const { getByText } = render( + + ); + expect(getByText('2.0 min')).toBeInTheDocument(); + act(() => { + fireEvent.click(getByText('disable series')); + }); + expect(getByText('120 s')).toBeInTheDocument(); + }); + it('falls back to the first formatter when disabled series is empty', () => { + const { getByText } = render( + + ); + expect(getByText('2.0 min')).toBeInTheDocument(); + act(() => { + fireEvent.click(getByText('disable series')); + }); + expect(getByText('2.0 min')).toBeInTheDocument(); + // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries); + // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); + // setDisabledSeriesState([true, true, false]); + // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); + }); + it('falls back to the first formatter when disabled series is all true', () => { + const { getByText } = render( + + ); + expect(getByText('2.0 min')).toBeInTheDocument(); + act(() => { + fireEvent.click(getByText('disable series')); + }); + expect(getByText('2.0 min')).toBeInTheDocument(); + // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries); + // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); + // setDisabledSeriesState([true, true, false]); + // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min'); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts new file mode 100644 index 0000000000000..8cd8929c89960 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts @@ -0,0 +1,30 @@ +/* + * 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, Dispatch, SetStateAction } from 'react'; +import { isEmpty } from 'lodash'; +import { + getDurationFormatter, + TimeFormatter, +} from '../../../../utils/formatters'; +import { TimeSeries } from '../../../../../typings/timeseries'; +import { getMaxY } from './helper'; + +export const useFormatter = ( + series: TimeSeries[] +): { + formatter: TimeFormatter; + setDisabledSeriesState: Dispatch>; +} => { + const [disabledSeriesState, setDisabledSeriesState] = useState([]); + const visibleSeries = series.filter( + (serie, index) => disabledSeriesState[index] !== true + ); + const maxY = getMaxY(isEmpty(visibleSeries) ? series : visibleSeries); + const formatter = getDurationFormatter(maxY); + + return { formatter, setDisabledSeriesState }; +}; diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx index 8c38cdcda958d..8334efffbd511 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx @@ -3,11 +3,12 @@ * 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 React, { ReactNode } from 'react'; +import { Observable, of } from 'rxjs'; import { ApmPluginContext, ApmPluginContextValue } from '.'; -import { createCallApmApi } from '../../services/rest/createCallApmApi'; import { ConfigSchema } from '../..'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; +import { createCallApmApi } from '../../services/rest/createCallApmApi'; const uiSettings: Record = { [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ @@ -33,8 +34,17 @@ const uiSettings: Record = { }; const mockCore = { + application: { + capabilities: { + apm: {}, + }, + currentAppId$: new Observable(), + }, chrome: { + docTitle: { change: () => {} }, setBreadcrumbs: () => {}, + setHelpExtension: () => {}, + setBadge: () => {}, }, docLinks: { DOC_LINK_VERSION: '0', @@ -45,6 +55,9 @@ const mockCore = { prepend: (path: string) => `/basepath${path}`, }, }, + i18n: { + Context: ({ children }: { children: ReactNode }) => children, + }, notifications: { toasts: { addWarning: () => {}, @@ -53,6 +66,7 @@ const mockCore = { }, uiSettings: { get: (key: string) => uiSettings[key], + get$: (key: string) => of(mockCore.uiSettings.get(key)), }, }; diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx index 37304d292540d..39d961f6a8164 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx @@ -4,16 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CoreStart } from 'kibana/public'; import { createContext } from 'react'; -import { AppMountContext } from 'kibana/public'; -import { ConfigSchema } from '../..'; +import { ConfigSchema } from '../../'; import { ApmPluginSetupDeps } from '../../plugin'; -export type AppMountContextBasePath = AppMountContext['core']['http']['basePath']; - export interface ApmPluginContextValue { config: ConfigSchema; - core: AppMountContext['core']; + core: CoreStart; plugins: ApmPluginSetupDeps; } diff --git a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx index f93b69a877057..801c1d7e53f2e 100644 --- a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx +++ b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx @@ -5,10 +5,10 @@ */ import React, { ReactNode, useMemo, useState } from 'react'; -import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; -import { history } from '../utils/history'; -import { useUrlParams } from '../hooks/useUrlParams'; +import { useHistory } from 'react-router-dom'; +import { fromQuery, toQuery } from '../components/shared/Links/url_helpers'; import { useFetcher } from '../hooks/useFetcher'; +import { useUrlParams } from '../hooks/useUrlParams'; const ChartsSyncContext = React.createContext<{ hoverX: number | null; @@ -18,6 +18,7 @@ const ChartsSyncContext = React.createContext<{ } | null>(null); function ChartsSyncContextProvider({ children }: { children: ReactNode }) { + const history = useHistory(); const [time, setTime] = useState(null); const { urlParams, uiFilters } = useUrlParams(); @@ -75,7 +76,7 @@ function ChartsSyncContextProvider({ children }: { children: ReactNode }) { }; return { ...hoverXHandlers }; - }, [time, data.annotations]); + }, [history, time, data.annotations]); return ; } diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 45ede7e7f2607..0ed26fe089487 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -5,21 +5,21 @@ */ import { omit } from 'lodash'; -import { useFetcher } from './useFetcher'; +import { useHistory } from 'react-router-dom'; +import { Projection } from '../../common/projections'; +import { pickKeys } from '../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters'; -import { useUrlParams } from './useUrlParams'; import { LocalUIFilterName, localUIFilters, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/ui_filters/local_ui_filters/config'; -import { history } from '../utils/history'; -import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; +import { fromQuery, toQuery } from '../components/shared/Links/url_helpers'; import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; -import { Projection } from '../../common/projections'; -import { pickKeys } from '../../common/utils/pick_keys'; import { useCallApi } from './useCallApi'; +import { useFetcher } from './useFetcher'; +import { useUrlParams } from './useUrlParams'; const getInitialData = ( filterNames: LocalUIFilterName[] @@ -39,6 +39,7 @@ export function useLocalUIFilters({ filterNames: LocalUIFilterName[]; params?: Record; }) { + const history = useHistory(); const { uiFilters, urlParams } = useUrlParams(); const callApi = useCallApi(); diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts index 2449c13f29435..9db78fde2d8c8 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts @@ -13,7 +13,7 @@ export function useTransactionBreakdown() { uiFilters, } = useUrlParams(); - const { data = { kpis: [], timeseries: [] }, error, status } = useFetcher( + const { data = { timeseries: [] }, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && transactionType) { return callApmApi({ diff --git a/x-pack/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/plugins/apm/public/services/__test__/callApi.test.ts index 76cc275aa8c23..f82201bbd4de8 100644 --- a/x-pack/plugins/apm/public/services/__test__/callApi.test.ts +++ b/x-pack/plugins/apm/public/services/__test__/callApi.test.ts @@ -23,7 +23,7 @@ describe('callApi', () => { }), } as unknown) as HttpMock; - // @ts-ignore + // @ts-expect-error global.sessionStorage = new SessionStorageMock(); }); diff --git a/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts index c4d59beb4b7a2..ca8a4919dd216 100644 --- a/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts +++ b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts @@ -20,9 +20,12 @@ describe('duration formatters', () => { '10,000 ms' ); expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20 s'); - expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('10 min'); + expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('600 s'); + expect(asDuration(toMicroseconds(11, 'minutes'))).toEqual('11 min'); expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60 min'); - expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('1.5 h'); + expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('90 min'); + expect(asDuration(toMicroseconds(10, 'hours'))).toEqual('600 min'); + expect(asDuration(toMicroseconds(11, 'hours'))).toEqual('11 h'); }); it('falls back to default value', () => { diff --git a/x-pack/plugins/apm/public/utils/formatters/duration.ts b/x-pack/plugins/apm/public/utils/formatters/duration.ts index 64a9e3b952b98..8381b0afb5f07 100644 --- a/x-pack/plugins/apm/public/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/public/utils/formatters/duration.ts @@ -127,10 +127,10 @@ export const toMicroseconds = (value: number, timeUnit: TimeUnit) => moment.duration(value, timeUnit).asMilliseconds() * 1000; function getDurationUnitKey(max: number): DurationTimeUnit { - if (max > toMicroseconds(1, 'hours')) { + if (max > toMicroseconds(10, 'hours')) { return 'hours'; } - if (max > toMicroseconds(1, 'minutes')) { + if (max > toMicroseconds(10, 'minutes')) { return 'minutes'; } if (max > toMicroseconds(10, 'seconds')) { diff --git a/x-pack/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/plugins/apm/public/utils/formatters/formatters.ts index 649f11063b149..6249ce53b6779 100644 --- a/x-pack/plugins/apm/public/utils/formatters/formatters.ts +++ b/x-pack/plugins/apm/public/utils/formatters/formatters.ts @@ -23,24 +23,3 @@ export function tpmUnit(type?: string) { defaultMessage: 'tpm', }); } - -export function asPercent( - numerator: number, - denominator: number | undefined, - fallbackResult = '' -) { - if (!denominator || isNaN(numerator)) { - return fallbackResult; - } - - const decimal = numerator / denominator; - - // 33.2 => 33% - // 3.32 => 3.3% - // 0 => 0% - if (Math.abs(decimal) >= 0.1 || decimal === 0) { - return numeral(decimal).format('0%'); - } - - return numeral(decimal).format('0.0%'); -} diff --git a/x-pack/plugins/apm/public/utils/history.ts b/x-pack/plugins/apm/public/utils/history.ts deleted file mode 100644 index bd2203fe92066..0000000000000 --- a/x-pack/plugins/apm/public/utils/history.ts +++ /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 { createHashHistory } from 'history'; - -// Make history singleton available across APM project -// TODO: Explore using React context or hook instead? -let history = createHashHistory(); - -export const resetHistory = () => { - history = createHashHistory(); -}; - -export { history }; diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index a750a9ea7af67..037da01c74464 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -26,6 +26,20 @@ import { } from '../../typings/elasticsearch'; import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; +const originalConsoleWarn = console.warn; // eslint-disable-line no-console +/** + * A dependency we're using is using deprecated react methods. Override the + * console to hide the warnings. These should go away when we switch to + * Elastic Charts + */ +export function disableConsoleWarning(messageToDisable: string) { + return jest.spyOn(console, 'warn').mockImplementation((message) => { + if (!message.startsWith(messageToDisable)) { + originalConsoleWarn(message); + } + }); +} + export function toJson(wrapper: ReactWrapper) { return enzymeToJson(wrapper, { noKey: true, diff --git a/x-pack/plugins/apm/scripts/tsconfig.json b/x-pack/plugins/apm/scripts/tsconfig.json index 350db55e72446..64602bc6b2769 100644 --- a/x-pack/plugins/apm/scripts/tsconfig.json +++ b/x-pack/plugins/apm/scripts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "include": [ "./**/*" ], diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 3abc89c470b21..e7eb7b8de65e3 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -73,10 +73,9 @@ export function registerTransactionDurationAnomalyAlertType({ return; } const alertParams = params as TypeOf; - const mlClient = services.getLegacyScopedClusterClient(ml.mlClient); - const request = { params: 'DummyKibanaRequest' } as KibanaRequest; - const { mlAnomalySearch } = ml.mlSystemProvider(mlClient, request); - const anomalyDetectors = ml.anomalyDetectorsProvider(mlClient, request); + const request = {} as KibanaRequest; + const { mlAnomalySearch } = ml.mlSystemProvider(request); + const anomalyDetectors = ml.anomalyDetectorsProvider(request); const mlJobIds = await getMLJobIds( anomalyDetectors, diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 29aaa98169fa5..8060bf10da99c 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -37,6 +37,9 @@ export async function getAllEnvironments({ ], }, body: { + // use timeout + min_doc_count to return as early as possible + // if filter is not defined to prevent timeouts + ...(!serviceName ? { timeout: '1ms' } : {}), size: 0, query: { bool: { @@ -48,6 +51,7 @@ export async function getAllEnvironments({ terms: { field: SERVICE_ENVIRONMENT, size: 100, + ...(!serviceName ? { min_doc_count: 0 } : {}), missing: includeMissing ? ENVIRONMENT_NOT_DEFINED.value : undefined, }, }, @@ -56,6 +60,7 @@ export async function getAllEnvironments({ }; const resp = await apmEventClient.search(params); + const environments = resp.aggregations?.environments.buckets.map( (bucket) => bucket.key as string diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts index c475640595227..9f7aaafbefb8c 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts @@ -34,7 +34,7 @@ export async function callClientWithDebug({ let res: any; let esError = null; try { - res = apiCaller(operationName, params); + res = await apiCaller(operationName, params); } catch (e) { // catch error and throw after outputting debug info esError = e; diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index 2e2f6c9a4c8b6..75b0471424e79 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -5,9 +5,9 @@ */ import moment from 'moment'; -// @ts-ignore +// @ts-expect-error import { calculateAuto } from './calculate_auto'; -// @ts-ignore +// @ts-expect-error import { unitToSeconds } from './unit_to_seconds'; export function getBucketSize(start: number, end: number, interval: string) { diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index a242a0adb6d4c..6b69e57389dff 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -122,13 +122,10 @@ function getMlSetup( if (!ml) { return; } - const mlClient = ml.mlClient.asScoped(request); - const mlSystem = ml.mlSystemProvider(mlClient, request); + return { - mlClient, - mlSystem, - modules: ml.modulesProvider(mlClient, request, savedObjectsClient), - anomalyDetectors: ml.anomalyDetectorsProvider(mlClient, request), - mlAnomalySearch: mlSystem.mlAnomalySearch, + mlSystem: ml.mlSystemProvider(request), + anomalyDetectors: ml.anomalyDetectorsProvider(request), + modules: ml.modulesProvider(request, savedObjectsClient), }; } diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index c5264373ea495..2cb28d378e8fd 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -10,15 +10,25 @@ Object { "body": Object { "aggs": Object { "backEnd": Object { - "avg": Object { + "percentiles": Object { "field": "transaction.marks.agent.timeToFirstByte", - "missing": 0, + "hdr": Object { + "number_of_significant_value_digits": 3, + }, + "percents": Array [ + 50, + ], }, }, "domInteractive": Object { - "avg": Object { + "percentiles": Object { "field": "transaction.marks.agent.domInteractive", - "missing": 0, + "hdr": Object { + "number_of_significant_value_digits": 3, + }, + "percents": Array [ + 50, + ], }, }, "pageViews": Object { @@ -138,7 +148,7 @@ Object { "body": Object { "aggs": Object { "pageViews": Object { - "aggs": Object {}, + "aggs": undefined, "auto_date_histogram": Object { "buckets": 50, "field": "@timestamp", diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index 194c136e2b3d0..e0dec183f06d6 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -11,6 +11,10 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { + TRANSACTION_DOM_INTERACTIVE, + TRANSACTION_TIME_TO_FIRST_BYTE, +} from '../../../common/elasticsearch_fieldnames'; export async function getClientMetrics({ setup, @@ -30,15 +34,21 @@ export async function getClientMetrics({ aggs: { pageViews: { value_count: { field: 'transaction.type' } }, backEnd: { - avg: { - field: 'transaction.marks.agent.timeToFirstByte', - missing: 0, + percentiles: { + field: TRANSACTION_TIME_TO_FIRST_BYTE, + percents: [50], + hdr: { + number_of_significant_value_digits: 3, + }, }, }, domInteractive: { - avg: { - field: 'transaction.marks.agent.domInteractive', - missing: 0, + percentiles: { + field: TRANSACTION_DOM_INTERACTIVE, + percents: [50], + hdr: { + number_of_significant_value_digits: 3, + }, }, }, }, @@ -53,9 +63,11 @@ export async function getClientMetrics({ // Divide by 1000 to convert ms into seconds return { pageViews, - backEnd: { value: (backEnd.value || 0) / 1000 }, + backEnd: { value: (backEnd.values['50.0'] || 0) / 1000 }, frontEnd: { - value: ((domInteractive.value || 0) - (backEnd.value || 0)) / 1000, + value: + ((domInteractive.values['50.0'] || 0) - (backEnd.values['50.0'] || 0)) / + 1000, }, }; } diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 23169ddaca534..114137e9fad17 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -11,7 +11,6 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { AggregationInputMap } from '../../../typings/elasticsearch/aggregations'; import { BreakdownItem } from '../../../typings/ui_filters'; export async function getPageViewTrends({ @@ -24,18 +23,9 @@ export async function getPageViewTrends({ const projection = getRumOverviewProjection({ setup, }); - const breakdownAggs: AggregationInputMap = {}; + let breakdownItem: BreakdownItem | null = null; if (breakdowns) { - const breakdownList: BreakdownItem[] = JSON.parse(breakdowns); - breakdownList.forEach(({ name, type, fieldName }) => { - breakdownAggs[name] = { - terms: { - field: fieldName, - size: 9, - missing: 'Other', - }, - }; - }); + breakdownItem = JSON.parse(breakdowns); } const params = mergeProjection(projection, { @@ -50,7 +40,17 @@ export async function getPageViewTrends({ field: '@timestamp', buckets: 50, }, - aggs: breakdownAggs, + aggs: breakdownItem + ? { + breakdown: { + terms: { + field: breakdownItem.fieldName, + size: 9, + missing: 'Other', + }, + }, + } + : undefined, }, }, }, @@ -68,19 +68,18 @@ export async function getPageViewTrends({ x: xVal, y: bCount, }; - - Object.keys(breakdownAggs).forEach((bKey) => { - const categoryBuckets = (bucket[bKey] as any).buckets; + if (breakdownItem) { + const categoryBuckets = (bucket.breakdown as any).buckets; categoryBuckets.forEach( ({ key, doc_count: docCount }: { key: string; doc_count: number }) => { if (key === 'Other') { - res[key + `(${bKey})`] = docCount; + res[key + `(${breakdownItem?.name})`] = docCount; } else { res[key] = docCount; } } ); - }); + } return res; }); diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 7e4bcfdda7382..e529198e717d3 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -72,7 +72,6 @@ describe('transformServiceMapResponses', () => { (element) => 'source' in element.data && 'target' in element.data ); - // @ts-ignore expect(connection?.data.target).toBe('opbeans-node'); expect( @@ -149,9 +148,9 @@ describe('transformServiceMapResponses', () => { const nodejsNode = nodes.find((node) => node.data.id === '>opbeans-node'); - // @ts-ignore + // @ts-expect-error expect(nodejsNode?.data[SPAN_TYPE]).toBe('external'); - // @ts-ignore + // @ts-expect-error expect(nodejsNode?.data[SPAN_SUBTYPE]).toBe('aa'); }); diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 2b465a0f87475..c01e5c87eeea2 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -127,11 +127,13 @@ Object { "services": Object { "terms": Object { "field": "service.name", + "min_doc_count": 0, "size": 50, }, }, }, "size": 0, + "timeout": "1ms", }, } `; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 8b6c1d82beab0..91bdfeef003f1 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -25,12 +25,14 @@ export async function getServiceNames({ setup }: { setup: Setup }) { ], }, body: { + timeout: '1ms', size: 0, aggs: { services: { terms: { field: SERVICE_NAME, size: 50, + min_doc_count: 0, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 2a1a581c79574..5c1e1839d9c53 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -65,7 +65,7 @@ function getItemsWithRelativeImpact( key: string | Record; avg?: number | null; count?: number | null; - p95?: number; + p95?: number | null; sample?: Transaction; }> ) { @@ -185,10 +185,12 @@ export async function transactionGroupsFetcher( } export interface TransactionGroup { - key: Record | string; + name?: string; + key?: Record | string; averageResponseTime: number | null | undefined; transactionsPerMinute: number; - p95: number | undefined; + p95: number | null | undefined; impact: number; + impactRelative?: number; sample: Transaction; } diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 731f75226cbe4..34863c64f9804 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -50,40 +50,9 @@ describe('getTransactionBreakdown', () => { setup: getMockSetup(noDataResponse), }); - expect(response.kpis.length).toBe(0); - expect(Object.keys(response.timeseries).length).toBe(0); }); - it('returns transaction breakdowns grouped by type and subtype', async () => { - const response = await getTransactionBreakdown({ - serviceName: 'myServiceName', - transactionType: 'request', - setup: getMockSetup(dataResponse), - }); - - expect(response.kpis.length).toBe(4); - - expect(response.kpis.map((kpi) => kpi.name)).toEqual([ - 'app', - 'dispatcher-servlet', - 'http', - 'postgresql', - ]); - - expect(response.kpis[0]).toEqual({ - name: 'app', - color: '#54b399', - percentage: 0.5408550899466306, - }); - - expect(response.kpis[3]).toEqual({ - name: 'postgresql', - color: '#9170b8', - percentage: 0.047366859295002, - }); - }); - it('returns a timeseries grouped by type and subtype', async () => { const response = await getTransactionBreakdown({ serviceName: 'myServiceName', @@ -98,7 +67,7 @@ describe('getTransactionBreakdown', () => { const appTimeseries = timeseries[0]; expect(appTimeseries.title).toBe('app'); expect(appTimeseries.type).toBe('areaStacked'); - expect(appTimeseries.hideLegend).toBe(true); + expect(appTimeseries.hideLegend).toBe(false); // empty buckets should result in null values for visible types expect(appTimeseries.data.length).toBe(276); @@ -110,7 +79,7 @@ describe('getTransactionBreakdown', () => { }); it('should not include more KPIs than MAX_KPIs', async () => { - // @ts-ignore + // @ts-expect-error constants.MAX_KPIS = 2; const response = await getTransactionBreakdown({ diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index fbdddea32deb4..9730ddbbf38d7 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -5,6 +5,7 @@ */ import { flatten, orderBy, last } from 'lodash'; +import { asPercent } from '../../../../common/utils/formatters'; import { ProcessorEvent } from '../../../../common/processor_event'; import { SERVICE_NAME, @@ -149,9 +150,16 @@ export async function getTransactionBreakdown({ ) : []; - const kpis = orderBy(visibleKpis, 'name').map((kpi, index) => { - return { + const kpis = orderBy( + visibleKpis.map((kpi) => ({ ...kpi, + lowerCaseName: kpi.name.toLowerCase(), + })), + 'lowerCaseName' + ).map((kpi, index) => { + const { lowerCaseName, ...rest } = kpi; + return { + ...rest, color: getVizColorForIndex(index), }; }); @@ -213,11 +221,9 @@ export async function getTransactionBreakdown({ color: kpi.color, type: 'areaStacked', data: timeseriesPerSubtype[kpi.name], - hideLegend: true, + hideLegend: false, + legendValue: asPercent(kpi.percentage, 1), })); - return { - kpis, - timeseries, - }; + return { timeseries }; } diff --git a/x-pack/plugins/apm/server/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts index 77a5beaf54605..332ac533e78c6 100644 --- a/x-pack/plugins/apm/server/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -15,7 +15,7 @@ export type Projection = Omit & { body: Omit & { aggs?: { [key: string]: { - terms: AggregationOptionsByType['terms']; + terms: AggregationOptionsByType['terms'] & { field: string }; aggs?: AggregationInputMap; }; }; diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index 92f52dd1552d6..42eebc51463db 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -140,7 +140,7 @@ export function createApi() { // Only return values for parameters that have runtime types, // but always include query as _debug is always set even if // it's not defined in the route. - // @ts-ignore + // @ts-expect-error params: pick(parsedParams, ...Object.keys(params), 'query'), config, logger, diff --git a/x-pack/plugins/apm/typings/cytoscape_dagre.d.ts b/x-pack/plugins/apm/typings/cytoscape_dagre.d.ts new file mode 100644 index 0000000000000..b5bbdfc14d9d3 --- /dev/null +++ b/x-pack/plugins/apm/typings/cytoscape_dagre.d.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. + */ + +declare module 'cytoscape-dagre'; diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index d25ec8709e3be..5e48f969c670a 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -23,13 +23,13 @@ type BucketsPath = string | Record; type SourceOptions = string | string[]; -type MetricsAggregationOptions = +type AggregationSourceOptions = | { field: string; - missing?: number; + missing?: unknown; } | { - script?: Script; + script: Script; }; interface MetricsAggregationResponsePart { @@ -56,43 +56,39 @@ type CompositeOptionsSource = Record< export interface AggregationOptionsByType { terms: { - field: string; size?: number; - missing?: string; order?: SortOptions; execution_hint?: 'map' | 'global_ordinals'; - }; + } & AggregationSourceOptions; date_histogram: { - field: string; format?: string; min_doc_count?: number; extended_bounds?: { min: number; max: number; }; - } & ({ calendar_interval: string } | { fixed_interval: string }); + } & ({ calendar_interval: string } | { fixed_interval: string }) & + AggregationSourceOptions; histogram: { - field: string; interval: number; min_doc_count?: number; extended_bounds?: { min?: number | string; max?: number | string; }; - }; - avg: MetricsAggregationOptions; - max: MetricsAggregationOptions; - min: MetricsAggregationOptions; - sum: MetricsAggregationOptions; - value_count: MetricsAggregationOptions; - cardinality: MetricsAggregationOptions & { + } & AggregationSourceOptions; + avg: AggregationSourceOptions; + max: AggregationSourceOptions; + min: AggregationSourceOptions; + sum: AggregationSourceOptions; + value_count: AggregationSourceOptions; + cardinality: AggregationSourceOptions & { precision_threshold?: number; }; percentiles: { - field: string; percents?: number[]; hdr?: { number_of_significant_value_digits: number }; - }; + } & AggregationSourceOptions; extended_stats: { field: string; }; @@ -133,7 +129,6 @@ export interface AggregationOptionsByType { reduce_script: Script; }; date_range: { - field: string; format?: string; ranges: Array< | { from: string | number } @@ -141,17 +136,15 @@ export interface AggregationOptionsByType { | { from: string | number; to: string | number } >; keyed?: boolean; - }; + } & AggregationSourceOptions; auto_date_histogram: { - field: string; buckets: number; - }; + } & AggregationSourceOptions; percentile_ranks: { - field: string; values: string[]; keyed?: boolean; hdr?: { number_of_significant_value_digits: number }; - }; + } & AggregationSourceOptions; } type AggregationType = keyof AggregationOptionsByType; @@ -223,7 +216,7 @@ interface AggregationResponsePart< value: number; }; percentiles: { - values: Record; + values: Record; }; extended_stats: { count: number; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts index 8e49d02beb908..b8eb79aabdcc5 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts @@ -12,7 +12,7 @@ import { Kubernetes } from './fields/kubernetes'; import { Page } from './fields/page'; import { Process } from './fields/process'; import { Service } from './fields/service'; -import { IStackframe } from './fields/stackframe'; +import { Stackframe } from './fields/stackframe'; import { Url } from './fields/url'; import { User } from './fields/user'; import { Observer } from './fields/observer'; @@ -23,16 +23,20 @@ interface Processor { } export interface Exception { + attributes?: { + response?: string; + }; + code?: string; message?: string; // either message or type are given type?: string; module?: string; handled?: boolean; - stacktrace?: IStackframe[]; + stacktrace?: Stackframe[]; } interface Log { message: string; - stacktrace?: IStackframe[]; + stacktrace?: Stackframe[]; } export interface ErrorRaw extends APMBaseDoc { diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/host.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/host.ts index 51c09e59d9b68..1869a4fc1bef5 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/host.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/host.ts @@ -5,5 +5,11 @@ */ export interface Host { + architecture?: string; hostname?: string; + name?: string; + ip?: string; + os?: { + platform?: string; + }; } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts index 0815b7cd88163..823d12cbd8095 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts @@ -5,7 +5,11 @@ */ export interface Observer { + ephemeral_id?: string; + hostname?: string; + id?: string; name?: string; + type?: string; version: string; version_major: number; } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/process.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/process.ts index 63e1faa382163..898ef04ed6a00 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/process.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/process.ts @@ -5,8 +5,8 @@ */ export interface Process { - args: string[]; + args?: string[]; pid: number; - ppid: number; - title: string; + ppid?: number; + title?: string; } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/service.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/service.ts index 3ef852ebf6dd6..00795d69e13b6 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/service.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/service.ts @@ -9,7 +9,10 @@ export interface Service { environment?: string; framework?: { name: string; - version: string; + version?: string; + }; + node?: { + name?: string; }; runtime?: { name: string; @@ -19,4 +22,5 @@ export interface Service { name: string; version?: string; }; + version?: string; } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/stackframe.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/stackframe.ts index 05b0eb88da40b..901b02814371a 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/stackframe.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/stackframe.ts @@ -4,27 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -type IStackframeBase = { - function?: string; - library_frame?: boolean; - exclude_from_grouping?: boolean; +interface Line { + column?: number; + number: number; +} + +interface Sourcemap { + error?: string; + updated?: boolean; +} + +interface StackframeBase { + abs_path?: string; + classname?: string; context?: { post?: string[]; pre?: string[]; }; + exclude_from_grouping?: boolean; + filename?: string; + function?: string; + module?: string; + library_frame?: boolean; + line?: Line; + sourcemap?: Sourcemap; vars?: { [key: string]: unknown; }; - line?: { - number: number; - }; -} & ({ classname: string } | { filename: string }); +} -export type IStackframeWithLineContext = IStackframeBase & { - line: { - number: number; +export type StackframeWithLineContext = StackframeBase & { + line: Line & { context: string; }; }; -export type IStackframe = IStackframeBase | IStackframeWithLineContext; +export type Stackframe = StackframeBase | StackframeWithLineContext; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index f6c4fce76f134..5c2e391059783 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -5,7 +5,7 @@ */ import { APMBaseDoc } from './apm_base_doc'; -import { IStackframe } from './fields/stackframe'; +import { Stackframe } from './fields/stackframe'; import { Observer } from './fields/observer'; interface Processor { @@ -24,7 +24,7 @@ export interface SpanRaw extends APMBaseDoc { duration: { us: number }; id: string; name: string; - stacktrace?: IStackframe[]; + stacktrace?: Stackframe[]; subtype?: string; sync?: boolean; type: string; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts index b8ebb4cf8da51..cdfe4183c96f5 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts @@ -54,6 +54,7 @@ export interface TransactionRaw extends APMBaseDoc { // Shared by errors and transactions container?: Container; + ecs?: { version?: string }; host?: Host; http?: Http; kubernetes?: Kubernetes; diff --git a/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts b/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts index 1e542dec06a72..4d98825f36b50 100644 --- a/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts +++ b/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts @@ -19,6 +19,7 @@ export type AgentName = | 'ruby'; export interface Agent { + ephemeral_id?: string; name: AgentName; version: string; } diff --git a/x-pack/plugins/apm/typings/ui_filters.ts b/x-pack/plugins/apm/typings/ui_filters.ts index 2a727dda7241d..efba6919778bb 100644 --- a/x-pack/plugins/apm/typings/ui_filters.ts +++ b/x-pack/plugins/apm/typings/ui_filters.ts @@ -14,7 +14,6 @@ export type UIFilters = { export interface BreakdownItem { name: string; - count: number; type: string; fieldName: string; selected?: boolean; diff --git a/x-pack/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/plugins/canvas/__tests__/fixtures/workpads.ts index 4b1f31cb14687..0d2939ed0e8a5 100644 --- a/x-pack/plugins/canvas/__tests__/fixtures/workpads.ts +++ b/x-pack/plugins/canvas/__tests__/fixtures/workpads.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 { CanvasWorkpad, CanvasElement, CanvasPage } from '../../types'; +import { CanvasWorkpad, CanvasElement, CanvasPage, CanvasVariable } from '../../types'; const BaseWorkpad: CanvasWorkpad = { '@created': '2019-02-08T18:35:23.029Z', @@ -50,6 +50,12 @@ const BaseElement: CanvasElement = { filter: '', }; +const BaseVariable: CanvasVariable = { + name: 'my-var', + value: 'Hello World', + type: 'string', +}; + export const workpads: CanvasWorkpad[] = [ { ...BaseWorkpad, @@ -71,6 +77,11 @@ export const workpads: CanvasWorkpad[] = [ ], }, ], + variables: [ + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, @@ -82,6 +93,11 @@ export const workpads: CanvasWorkpad[] = [ ], }, ], + variables: [ + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, @@ -103,6 +119,11 @@ export const workpads: CanvasWorkpad[] = [ { ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] }, { ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] }, ], + variables: [ + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, @@ -136,6 +157,11 @@ export const workpads: CanvasWorkpad[] = [ ], }, ], + variables: [ + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, @@ -166,6 +192,17 @@ export const workpads: CanvasWorkpad[] = [ ], }, ], + variables: [ + { + ...BaseVariable, + }, + { + ...BaseVariable, + }, + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts index 16eee349475ef..11551c50d9f25 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts @@ -39,9 +39,9 @@ interface PieOptions { colors: string[]; legend: { show: boolean; - backgroundOpacity: number; - labelBoxBorderColor: string; - position: Legend; + backgroundOpacity?: number; + labelBoxBorderColor?: string; + position?: Legend; }; grid: { show: boolean; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts index 882d1e2ea58b9..33260b5c9303f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.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. */ -jest.mock('ui/new_platform'); + import { savedLens } from './saved_lens'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts index 74e41a030de35..b2de0b6526d1e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.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. */ -jest.mock('ui/new_platform'); + import { savedMap } from './saved_map'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts index 9bd32202b563a..7d5952754aa90 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.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. */ -jest.mock('ui/new_platform'); + import { savedSearch } from './saved_search'; import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts index 8327c1433b9af..a64fb167dd19f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.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. */ -jest.mock('ui/new_platform'); + import { savedVisualization } from './saved_visualization'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot new file mode 100644 index 0000000000000..b9bc21dd6e3ea --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/image default 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot new file mode 100644 index 0000000000000..9b97ae1fdacb3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/repeatImage default 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot new file mode 100644 index 0000000000000..cf9cc6dd82f7f --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/table default 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx new file mode 100644 index 0000000000000..bcd8365034448 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx @@ -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 React from 'react'; +import { storiesOf } from '@storybook/react'; +import { image } from '../image'; +import { Render } from './render'; +import { elasticLogo } from '../../lib/elastic_logo'; + +storiesOf('renderers/image', module).add('default', () => { + const config = { + type: 'image' as 'image', + mode: 'cover', + dataurl: elasticLogo, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx new file mode 100644 index 0000000000000..647c63c2c1042 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx @@ -0,0 +1,65 @@ +/* + * 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 { action } from '@storybook/addon-actions'; +import React, { useRef, useEffect } from 'react'; +import { RendererFactory, RendererHandlers } from '../../../types'; + +export const defaultHandlers: RendererHandlers = { + destroy: () => action('destroy'), + getElementId: () => 'element-id', + getFilter: () => 'filter', + onComplete: (fn) => undefined, + onEmbeddableDestroyed: action('onEmbeddableDestroyed'), + onEmbeddableInputChange: action('onEmbeddableInputChange'), + onResize: action('onResize'), + resize: action('resize'), + setFilter: action('setFilter'), + done: action('done'), + onDestroy: action('onDestroy'), + reload: action('reload'), + update: action('update'), + event: action('event'), +}; + +/* + Uses a RenderDefinitionFactory and Config to render into an element. + + Intended to be used for stories for RenderDefinitionFactory +*/ +interface RenderAdditionalProps { + height?: string; + width?: string; + handlers?: RendererHandlers; +} + +export const Render = ({ + renderer, + config, + ...rest +}: Renderer extends RendererFactory + ? { renderer: Renderer; config: Config } & RenderAdditionalProps + : { renderer: undefined; config: undefined } & RenderAdditionalProps) => { + const { height, width, handlers } = { + height: '200px', + width: '200px', + handlers: defaultHandlers, + ...rest, + }; + + const containerRef = useRef(null); + + useEffect(() => { + if (renderer && containerRef.current !== null) { + renderer().render(containerRef.current, config, handlers); + } + }, [renderer, config, handlers]); + + return ( +
    + {' '} +
    + ); +}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx new file mode 100644 index 0000000000000..41ccc054a77fb --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx @@ -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 React from 'react'; +import { storiesOf } from '@storybook/react'; +import { repeatImage } from '../repeat_image'; +import { Render } from './render'; +import { elasticLogo } from '../../lib/elastic_logo'; +import { elasticOutline } from '../../lib/elastic_outline'; + +storiesOf('renderers/repeatImage', module).add('default', () => { + const config = { + count: 42, + image: elasticLogo, + size: 20, + max: 60, + emptyImage: elasticOutline, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.tsx new file mode 100644 index 0000000000000..f3c70cb30de45 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.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 React from 'react'; +import { storiesOf } from '@storybook/react'; +import { table } from '../table'; +import { Render } from './render'; + +storiesOf('renderers/table', module).add('default', () => { + const config = { + paginate: true, + perPage: 5, + showHeader: true, + datatable: { + type: 'datatable' as 'datatable', + columns: [ + { + name: 'Foo', + type: 'string' as 'string', + id: 'id-foo', + meta: { type: 'string' as 'string' }, + }, + { + name: 'Bar', + type: 'number' as 'number', + id: 'id-bar', + meta: { type: 'string' as 'string' }, + }, + ], + rows: [ + { Foo: 'a', Bar: 700 }, + { Foo: 'b', Bar: 600 }, + { Foo: 'c', Bar: 500 }, + { Foo: 'd', Bar: 400 }, + { Foo: 'e', Bar: 300 }, + { Foo: 'f', Bar: 200 }, + { Foo: 'g', Bar: 100 }, + ], + }, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot new file mode 100644 index 0000000000000..b7039ee1847c7 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/error default 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx new file mode 100644 index 0000000000000..c71999bc04bb1 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx @@ -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 React from 'react'; +import { storiesOf } from '@storybook/react'; +import { error } from '../'; +import { Render } from '../../__stories__/render'; + +storiesOf('renderers/error', module).add('default', () => { + const thrownError = new Error('There was an error'); + const config = { + error: thrownError, + }; + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot new file mode 100644 index 0000000000000..79f447c953d6d --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/markdown default 1`] = ` +
    + +
    +`; + +exports[`Storyshots renderers/markdown links in new tab 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.tsx new file mode 100644 index 0000000000000..d5b190c74a92f --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { markdown } from '../'; +import { Render } from '../../__stories__/render'; + +storiesOf('renderers/markdown', module) + .add('default', () => { + const config = { + content: '# This is Markdown', + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + openLinksInNewTab: false, + }; + return ; + }) + .add('links in new tab', () => { + const config = { + content: '[Elastic.co](https://elastic.co)', + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + openLinksInNewTab: true, + }; + return ; + }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot new file mode 100644 index 0000000000000..3260dbe8c83c2 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/pie default 1`] = ` +
    + +
    +`; + +exports[`Storyshots renderers/pie with legend 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx new file mode 100644 index 0000000000000..dea2876de0ec8 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { pie } from '../'; +import { Render } from '../../__stories__/render'; + +const pieOptions = { + canvas: false, + colors: ['#882E72', '#B178A6', '#D6C1DE'], + grid: { show: false }, + legend: { show: false }, + series: { + pie: { + show: true, + innerRadius: 0, + label: { show: true, radius: 1 }, + radius: 'auto' as 'auto', + stroke: { width: 0 }, + tilt: 1, + }, + }, +}; + +const data = [ + { + data: [10], + label: 'A', + }, + { + data: [10], + label: 'B', + }, + { + data: [10], + label: 'C', + }, +]; + +storiesOf('renderers/pie', module) + .add('default', () => { + const config = { + data, + options: pieOptions, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + }; + return ; + }) + .add('with legend', () => { + const options = { + ...pieOptions, + legend: { show: true }, + }; + + const config = { + data, + options, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + }; + + return ; + }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot new file mode 100644 index 0000000000000..7419d13fc7195 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/plot default 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx new file mode 100644 index 0000000000000..0e9566d2a5c20 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx @@ -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 React from 'react'; +import { storiesOf } from '@storybook/react'; +import { plot } from '../'; +import { Render } from '../../__stories__/render'; + +const plotOptions = { + canvas: false, + colors: ['#882E72', '#B178A6', '#D6C1DE'], + grid: { + margin: { + bottom: 0, + left: 0, + right: 30, + top: 20, + }, + }, + legend: { show: true }, + series: { + bubbles: { + show: true, + fill: false, + }, + }, + xaxis: { + show: true, + mode: 'time', + }, + yaxis: { + show: true, + }, +}; + +const data = [ + { + bubbles: { show: true }, + data: [ + [1546351551031, 33, { size: 5 }], + [1546351551131, 38, { size: 2 }], + ], + label: 'done', + }, + { + bubbles: { show: true }, + data: [ + [1546351551032, 37, { size: 4 }], + [1546351551139, 45, { size: 3 }], + ], + label: 'running', + }, +]; + +storiesOf('renderers/plot', module).add('default', () => { + const config = { + data, + options: plotOptions, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + }; + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot new file mode 100644 index 0000000000000..1fe884656ef3b --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/progress default 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx new file mode 100644 index 0000000000000..3e20cfd4772a8 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx @@ -0,0 +1,30 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { progress } from '../'; +import { Render } from '../../__stories__/render'; +import { Shape } from '../../../functions/common/progress'; + +storiesOf('renderers/progress', module).add('default', () => { + const config = { + barColor: '#bc1234', + barWeight: 20, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + label: '66%', + max: 1, + shape: Shape.UNICORN, + value: 0.66, + valueColor: '#000', + valueWeight: 15, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot new file mode 100644 index 0000000000000..b9963565a09f5 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/revealImage default 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx new file mode 100644 index 0000000000000..637f9a2bb6986 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx @@ -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 React from 'react'; +import { storiesOf } from '@storybook/react'; +import { revealImage } from '../'; +import { Render } from '../../__stories__/render'; +import { elasticOutline } from '../../../lib/elastic_outline'; +import { elasticLogo } from '../../../lib/elastic_logo'; +import { Origin } from '../../../functions/common/revealImage'; + +storiesOf('renderers/revealImage', module).add('default', () => { + const config = { + image: elasticLogo, + emptyImage: elasticOutline, + origin: Origin.LEFT, + percent: 0.45, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot new file mode 100644 index 0000000000000..317c20021015a --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/shape default 1`] = ` +
    + +
    +`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx new file mode 100644 index 0000000000000..19df14c08688f --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx @@ -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 React from 'react'; +import { storiesOf } from '@storybook/react'; +import { shape } from '../'; +import { Render } from '../../__stories__/render'; +import { Shape } from '../../../functions/common/shape'; + +storiesOf('renderers/shape', module).add('default', () => { + const config = { + type: 'shape' as 'shape', + border: '#FFEEDD', + borderWidth: 8, + shape: Shape.BOOKMARK, + fill: '#112233', + maintainAspect: true, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/common/lib/autocomplete.test.ts b/x-pack/plugins/canvas/common/lib/autocomplete.test.ts index f7a773f6ca36a..777810cad05ba 100644 --- a/x-pack/plugins/canvas/common/lib/autocomplete.test.ts +++ b/x-pack/plugins/canvas/common/lib/autocomplete.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); import { functionSpecs } from '../../__tests__/fixtures/function_specs'; import { diff --git a/x-pack/plugins/canvas/public/__tests__/setup.js b/x-pack/plugins/canvas/public/__tests__/setup.js deleted file mode 100644 index 1792397d0614e..0000000000000 --- a/x-pack/plugins/canvas/public/__tests__/setup.js +++ /dev/null @@ -1,15 +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 enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; - -// this will run before any code that's inside a describe block -// so we can use it to set up whatever we need for our browser tests -before(() => { - // configure enzume - enzyme.configure({ adapter: new Adapter() }); -}); diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx index e8b772e0f2fbd..b037d30ada12f 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx @@ -14,7 +14,6 @@ import { import { METRIC_TYPE } from '../../../lib/ui_metric'; import { ResolvedArgType } from '../../../../types'; -jest.mock('ui/new_platform'); const trackMetric = jest.fn(); const Component = withUnconnectedElementsLoadedTelemetry(() =>
    , trackMetric); diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts deleted file mode 100644 index cb46a3d6be402..0000000000000 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts +++ /dev/null @@ -1,218 +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 { select } from '../../select'; -import { Json, Selector, Vector2d, Vector3d, TransformMatrix2d, TransformMatrix3d } from '../..'; -import { - mvMultiply as mult2d, - ORIGIN as UNIT2D, - UNITMATRIX as UNITMATRIX2D, - add as add2d, -} from '../../matrix2d'; -import { - mvMultiply as mult3d, - ORIGIN as UNIT3D, - NANMATRIX as NANMATRIX3D, - add as add3d, -} from '../../matrix'; - -/* - - Type checking isn't too useful if future commits can accidentally weaken the type constraints, because a - TypeScript linter will not complain - everything that passed before will continue to pass. The coder - will not have feedback that the original intent with the typing got compromised. To declare the intent - via passing and failing type checks, test cases are needed, some of which designed to expect a TS pass, - some of them to expect a TS complaint. It documents intent for peers too, as type specs are a tough read. - - Run compile-time type specification tests in the `kibana` root with: - - yarn typespec - - Test "cases" expecting to pass TS checks are not annotated, while ones we want TS to complain about - are prepended with the comment - - // typings:expect-error - - The test "suite" and "cases" are wrapped in IIFEs to prevent linters from complaining about the unused - binding. It can be structured internally as desired. - -*/ - -((): void => { - /** - * TYPE TEST SUITE - */ - - (function vectorArrayCreationTests(vec2d: Vector2d, vec3d: Vector3d): void { - // 2D vector OK - vec2d = [0, 0, 0] as Vector2d; // OK - vec2d = [-0, NaN, -Infinity] as Vector2d; // IEEE 754 values are OK - - // 3D vector OK - vec3d = [0, 0, 0, 0] as Vector3d; - vec3d = [100, -0, Infinity, NaN] as Vector3d; - - // 2D vector not OK - - // typings:expect-error - vec2d = 3; // not even an array - // typings:expect-error - vec2d = [] as Vector2d; // no elements - // typings:expect-error - vec2d = [0, 0] as Vector2d; // too few elements - // typings:expect-error - vec2d = [0, 0, 0, 0] as Vector2d; // too many elements - - // 3D vector not OK - - // typings:expect-error - vec3d = 3; // not even an array - // typings:expect-error - vec3d = [] as Vector3d; // no elements - // typings:expect-error - vec3d = [0, 0, 0] as Vector3d; // too few elements - // typings:expect-error - vec3d = [0, 0, 0, 0, 0] as Vector3d; // too many elements - - return; // arrayCreationTests - })(UNIT2D, UNIT3D); - - (function matrixArrayCreationTests(mat2d: TransformMatrix2d, mat3d: TransformMatrix3d): void { - // 2D matrix OK - mat2d = [0, 1, 2, 3, 4, 5, 6, 7, 8] as TransformMatrix2d; // OK - mat2d = [-0, NaN, -Infinity, 3, 4, 5, 6, 7, 8] as TransformMatrix2d; // IEEE 754 values are OK - - // 3D matrix OK - mat3d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as TransformMatrix3d; - mat3d = [100, -0, Infinity, NaN, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as TransformMatrix3d; - - // 2D matrix not OK - - // typings:expect-error - mat2d = 3; // not even an array - // typings:expect-error - mat2d = [] as TransformMatrix2d; // no elements - // typings:expect-error - mat2d = [0, 1, 2, 3, 4, 5, 6, 7] as TransformMatrix2d; // too few elements - // typings:expect-error - mat2d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as TransformMatrix2d; // too many elements - - // 3D vector not OK - - // typings:expect-error - mat3d = 3; // not even an array - // typings:expect-error - mat3d = [] as TransformMatrix3d; // no elements - // typings:expect-error - mat3d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] as TransformMatrix3d; // too few elements - // typings:expect-error - mat3d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] as TransformMatrix3d; // too many elements - - // Matrix modification should NOT be OK - mat3d[3] = 100; // too bad the ReadOnly part appears not to be enforced so can't precede it with typings:expect-error - - return; // arrayCreationTests - })(UNITMATRIX2D, NANMATRIX3D); - - (function matrixMatrixAdditionTests(mat2d: TransformMatrix2d, mat3d: TransformMatrix3d): void { - add2d(mat2d, mat2d); // OK - add3d(mat3d, mat3d); // OK - - // typings:expect-error - add2d(mat2d, mat3d); // at least one arg doesn't comply - // typings:expect-error - add2d(mat3d, mat2d); // at least one arg doesn't comply - // typings:expect-error - add2d(mat3d, mat3d); // at least one arg doesn't comply - // typings:expect-error - add3d(mat2d, mat3d); // at least one arg doesn't comply - // typings:expect-error - add3d(mat3d, mat2d); // at least one arg doesn't comply - // typings:expect-error - add3d(mat2d, mat2d); // at least one arg doesn't comply - - return; // matrixMatrixAdditionTests - })(UNITMATRIX2D, NANMATRIX3D); - - (function matrixVectorMultiplicationTests( - vec2d: Vector2d, - mat2d: TransformMatrix2d, - vec3d: Vector3d, - mat3d: TransformMatrix3d - ): void { - mult2d(mat2d, vec2d); // OK - mult3d(mat3d, vec3d); // OK - - // typings:expect-error - mult3d(mat2d, vec2d); // trying to use a 3d fun for 2d args - // typings:expect-error - mult2d(mat3d, vec3d); // trying to use a 2d fun for 3d args - - // typings:expect-error - mult2d(mat3d, vec2d); // 1st arg is a mismatch - // typings:expect-error - mult2d(mat2d, vec3d); // 2nd arg is a mismatch - - // typings:expect-error - mult3d(mat2d, vec3d); // 1st arg is a mismatch - // typings:expect-error - mult3d(mat3d, vec2d); // 2nd arg is a mismatch - - return; // matrixVectorTests - })(UNIT2D, UNITMATRIX2D, UNIT3D, NANMATRIX3D); - - (function jsonTests(plain: Json): void { - // numbers are OK - plain = 1; - plain = NaN; - plain = Infinity; - plain = -Infinity; - plain = Math.pow(2, 6); - // other JSON primitive types are OK - plain = false; - plain = 'hello'; - plain = null; - // structures made of above and of structures are OK - plain = {}; - plain = []; - plain = { a: 1 }; - plain = [0, null, false, NaN, 3.14, 'one more']; - plain = { a: { b: 5, c: { d: [1, 'a', -Infinity, null], e: -1 }, f: 'b' }, g: false }; - - // typings:expect-error - plain = undefined; // it's undefined - // typings:expect-error - plain = (a) => a; // it's a function - // typings:expect-error - plain = [new Date()]; // it's a time - // typings:expect-error - plain = { a: Symbol('haha') }; // symbol isn't permitted either - // typings:expect-error - plain = window || void 0; - // typings:expect-error - plain = { a: { b: 5, c: { d: [1, 'a', undefined, null] } } }; // going deep into the structure - - return; // jsonTests - })(null); - - (function selectTests(selector: Selector): void { - selector = select((a: Json) => a); // one arg - selector = select((a: Json, b: Json): Json => `${a} and ${b}`); // more args - selector = select(() => 1); // zero arg - selector = select((...args: Json[]) => args); // variadic - - // typings:expect-error - selector = (a: Json) => a; // not a selector - // typings:expect-error - selector = select(() => {}); // should yield a JSON value, but it returns void - // typings:expect-error - selector = select((x: Json) => ({ a: x, b: undefined })); // should return a Json - - return; // selectTests - })(select((a: Json) => a)); - - return; // test suite -})(); diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json b/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json deleted file mode 100644 index 3b61e4b414626..0000000000000 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "../../../../../tsconfig", - "compilerOptions": { - "module": "commonjs", - "lib": ["es2018", "dom"], - "noImplicitAny": true, - "strictNullChecks": true, - "strictFunctionTypes": false, - "strictPropertyInitialization": true, - "noImplicitThis": true, - "noImplicitReturns": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "baseUrl": ".", - "paths": { - "layout/*": ["aeroelastic/*"] - }, - "types": ["@kbn/x-pack/plugins/canvas/public/lib/aeroelastic"] - }, - "exclude": ["node_modules", "**/*.spec.ts", "node_modules/@types/mocha"] -} diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/typespec.test.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/typespec.test.ts new file mode 100644 index 0000000000000..3b1fde12edcc4 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/typespec.test.ts @@ -0,0 +1,228 @@ +/* + * 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 { select } from './select'; +import { Json, Selector, Vector2d, Vector3d, TransformMatrix2d, TransformMatrix3d } from './index'; +import { + mvMultiply as mult2d, + ORIGIN as UNIT2D, + UNITMATRIX as UNITMATRIX2D, + add as add2d, +} from './matrix2d'; +import { + mvMultiply as mult3d, + ORIGIN as UNIT3D, + NANMATRIX as NANMATRIX3D, + add as add3d, +} from './matrix'; + +// helper to mark variables as "used" so they don't trigger errors +const use = (...vars: any[]) => vars.includes(null); + +/* + Type checking isn't too useful if future commits can accidentally weaken the type constraints, because a + TypeScript linter will not complain - everything that passed before will continue to pass. The coder + will not have feedback that the original intent with the typing got compromised. To declare the intent + via passing and failing type checks, test cases are needed, some of which designed to expect a TS pass, + some of them to expect a TS complaint. It documents intent for peers too, as type specs are a tough read. + + Test "cases" expecting to pass TS checks are not annotated, while ones we want TS to complain about + are prepended with the comment + + // @ts-expect-error + + The test "suite" and "cases" are wrapped in IIFEs to prevent linters from complaining about the unused + binding. It can be structured internally as desired. +*/ + +describe('vector array creation', () => { + it('passes typechecking', () => { + let vec2d: Vector2d = UNIT2D; + let vec3d: Vector3d = UNIT3D; + + use(vec2d, vec3d); + + // 2D vector OK + vec2d = [0, 0, 0] as Vector2d; // OK + vec2d = [-0, NaN, -Infinity] as Vector2d; // IEEE 754 values are OK + + // 3D vector OK + vec3d = [0, 0, 0, 0] as Vector3d; + vec3d = [100, -0, Infinity, NaN] as Vector3d; + + // 2D vector not OK + + // @ts-expect-error + vec2d = 3; // not even an array + // @ts-expect-error + vec2d = [] as Vector2d; // no elements + // @ts-expect-error + vec2d = [0, 0] as Vector2d; // too few elements + // @ts-expect-error + vec2d = [0, 0, 0, 0] as Vector2d; // too many elements + + // 3D vector not OK + + // @ts-expect-error + vec3d = 3; // not even an array + // @ts-expect-error + vec3d = [] as Vector3d; // no elements + // @ts-expect-error + vec3d = [0, 0, 0] as Vector3d; // too few elements + // @ts-expect-error + vec3d = [0, 0, 0, 0, 0] as Vector3d; // too many elements + }); +}); + +describe('matrix array creation', () => { + it('passes typechecking', () => { + let mat2d: TransformMatrix2d = UNITMATRIX2D; + let mat3d: TransformMatrix3d = NANMATRIX3D; + + use(mat2d, mat3d); + + // 2D matrix OK + mat2d = [0, 1, 2, 3, 4, 5, 6, 7, 8] as TransformMatrix2d; // OK + mat2d = [-0, NaN, -Infinity, 3, 4, 5, 6, 7, 8] as TransformMatrix2d; // IEEE 754 values are OK + + // 3D matrix OK + mat3d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as TransformMatrix3d; + mat3d = [100, -0, Infinity, NaN, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as TransformMatrix3d; + + // 2D matrix not OK + + // @ts-expect-error + mat2d = 3; // not even an array + // @ts-expect-error + mat2d = [] as TransformMatrix2d; // no elements + // @ts-expect-error + mat2d = [0, 1, 2, 3, 4, 5, 6, 7] as TransformMatrix2d; // too few elements + // @ts-expect-error + mat2d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as TransformMatrix2d; // too many elements + + // 3D vector not OK + + // @ts-expect-error + mat3d = 3; // not even an array + // @ts-expect-error + mat3d = [] as TransformMatrix3d; // no elements + // @ts-expect-error + mat3d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] as TransformMatrix3d; // too few elements + // @ts-expect-error + mat3d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] as TransformMatrix3d; // too many elements + + // @ts-expect-error + mat3d[3] = 100; // Matrix modification is NOT OK + }); +}); + +describe('matrix addition', () => { + it('passes typecheck', () => { + const mat2d: TransformMatrix2d = UNITMATRIX2D; + const mat3d: TransformMatrix3d = NANMATRIX3D; + + add2d(mat2d, mat2d); // OK + add3d(mat3d, mat3d); // OK + + // @ts-expect-error + add2d(mat2d, mat3d); // at least one arg doesn't comply + // @ts-expect-error + add2d(mat3d, mat2d); // at least one arg doesn't comply + // @ts-expect-error + add2d(mat3d, mat3d); // at least one arg doesn't comply + // @ts-expect-error + add3d(mat2d, mat3d); // at least one arg doesn't comply + // @ts-expect-error + add3d(mat3d, mat2d); // at least one arg doesn't comply + // @ts-expect-error + add3d(mat2d, mat2d); // at least one arg doesn't comply + }); +}); + +describe('matric vector multiplication', () => { + it('passes typecheck', () => { + const vec2d: Vector2d = UNIT2D; + const mat2d: TransformMatrix2d = UNITMATRIX2D; + const vec3d: Vector3d = UNIT3D; + const mat3d: TransformMatrix3d = NANMATRIX3D; + + mult2d(mat2d, vec2d); // OK + mult3d(mat3d, vec3d); // OK + + // @ts-expect-error + mult3d(mat2d, vec2d); // trying to use a 3d fun for 2d args + // @ts-expect-error + mult2d(mat3d, vec3d); // trying to use a 2d fun for 3d args + + // @ts-expect-error + mult2d(mat3d, vec2d); // 1st arg is a mismatch + // @ts-expect-error + mult2d(mat2d, vec3d); // 2nd arg is a mismatch + + // @ts-expect-error + mult3d(mat2d, vec3d); // 1st arg is a mismatch + // @ts-expect-error + mult3d(mat3d, vec2d); // 2nd arg is a mismatch + }); +}); + +describe('json', () => { + it('passes typecheck', () => { + let plain: Json = null; + + use(plain); + + // numbers are OK + plain = 1; + plain = NaN; + plain = Infinity; + plain = -Infinity; + plain = Math.pow(2, 6); + // other JSON primitive types are OK + plain = false; + plain = 'hello'; + plain = null; + // structures made of above and of structures are OK + plain = {}; + plain = []; + plain = { a: 1 }; + plain = [0, null, false, NaN, 3.14, 'one more']; + plain = { a: { b: 5, c: { d: [1, 'a', -Infinity, null], e: -1 }, f: 'b' }, g: false }; + + // @ts-expect-error + plain = undefined; // it's undefined + // @ts-expect-error + plain = (a) => a; // it's a function + // @ts-expect-error + plain = [new Date()]; // it's a time + // @ts-expect-error + plain = { a: Symbol('haha') }; // symbol isn't permitted either + // @ts-expect-error + plain = window || void 0; + // @ts-expect-error + plain = { a: { b: 5, c: { d: [1, 'a', undefined, null] } } }; // going deep into the structure + }); +}); + +describe('select', () => { + it('passes typecheck', () => { + let selector: Selector; + + selector = select((a: Json) => a); // one arg + selector = select((a: Json, b: Json): Json => `${a} and ${b}`); // more args + selector = select(() => 1); // zero arg + selector = select((...args: Json[]) => args); // variadic + + // @ts-expect-error + selector = (a: Json) => a; // not a selector + // @ts-expect-error + selector = select(() => {}); // should yield a JSON value, but it returns void + // @ts-expect-error + selector = select((x: Json) => ({ a: x, b: undefined })); // should return a Json + + use(selector); + }); +}); diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts index bf69a862d5c30..e451a39df06db 100644 --- a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts +++ b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); // actions/elements has some dependencies on ui/new_platform. jest.mock('../../../lib/app_state'); import { workpadRefresh } from '../workpad_refresh'; diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts b/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts index 5b1192630897a..08b59e5fedc44 100644 --- a/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts +++ b/x-pack/plugins/canvas/public/state/reducers/embeddables.test.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. */ -jest.mock('ui/new_platform'); + import { State } from '../../../types'; import { updateEmbeddableExpression } from '../actions/embeddable'; import { embeddableReducer } from './embeddable'; diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts index 9f71edcc05bf2..32665cc42dc4e 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts @@ -49,6 +49,14 @@ describe('usage collector handle es response data', () => { 'shape', ], }, + variables: { + total: 7, + per_workpad: { + avg: 1.1666666666666667, + min: 0, + max: 3, + }, + }, }); }); @@ -63,6 +71,7 @@ describe('usage collector handle es response data', () => { pages: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } }, elements: { total: 1, per_page: { avg: 1, min: 1, max: 1 } }, functions: { total: 1, in_use: ['toast'], per_element: { avg: 1, min: 1, max: 1 } }, + variables: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } }, }); }); @@ -76,6 +85,39 @@ describe('usage collector handle es response data', () => { pages: { total: 0, per_workpad: { avg: 0, min: 0, max: 0 } }, elements: undefined, functions: undefined, + variables: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } }, // Variables still possible even with no pages + }); + }); + + it('should handle cases where the version workpad might not have variables', () => { + const workpad = cloneDeep(workpads[0]); + // @ts-ignore + workpad.variables = undefined; + + const mockWorkpadsOld = [workpad]; + const usage = summarizeWorkpads(mockWorkpadsOld); + expect(usage).toEqual({ + workpads: { total: 1 }, + pages: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } }, + elements: { total: 1, per_page: { avg: 1, min: 1, max: 1 } }, + functions: { + total: 7, + in_use: [ + 'demodata', + 'ply', + 'rowCount', + 'as', + 'staticColumn', + 'math', + 'mapColumn', + 'sort', + 'pointseries', + 'plot', + 'seriesStyle', + ], + per_element: { avg: 7, min: 7, max: 7 }, + }, + variables: { total: 0, per_workpad: { avg: 0, min: 0, max: 0 } }, // Variables still possible even with no pages }); }); diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts index 4b00d061c17ce..9fa39c580962d 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts @@ -44,6 +44,14 @@ interface WorkpadTelemetry { max: number; }; }; + variables?: { + total: number; + per_workpad: { + avg: number; + min: number; + max: number; + }; + }; } /** @@ -81,7 +89,10 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr }); }, []); - return { pages, elementCounts, functionCounts }; + const variableCount = + workpad.variables && workpad.variables.length ? workpad.variables.length : 0; + + return { pages, elementCounts, functionCounts, variableCount }; }); // combine together info from across the workpads @@ -91,9 +102,10 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr pageCounts: number[]; elementCounts: number[]; functionCounts: number[]; + variableCounts: number[]; }>( (accum, pageInfo) => { - const { pages, elementCounts, functionCounts } = pageInfo; + const { pages, elementCounts, functionCounts, variableCount } = pageInfo; return { pageMin: pages.count < accum.pageMin ? pages.count : accum.pageMin, @@ -101,6 +113,7 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr pageCounts: accum.pageCounts.concat(pages.count), elementCounts: accum.elementCounts.concat(elementCounts), functionCounts: accum.functionCounts.concat(functionCounts), + variableCounts: accum.variableCounts.concat([variableCount]), }; }, { @@ -109,13 +122,23 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr pageCounts: [], elementCounts: [], functionCounts: [], + variableCounts: [], } ); - const { pageCounts, pageMin, pageMax, elementCounts, functionCounts } = combinedWorkpadsInfo; + const { + pageCounts, + pageMin, + pageMax, + elementCounts, + functionCounts, + variableCounts, + } = combinedWorkpadsInfo; const pageTotal = arraySum(pageCounts); const elementsTotal = arraySum(elementCounts); const functionsTotal = arraySum(functionCounts); + const variableTotal = arraySum(variableCounts); + const pagesInfo = workpadsInfo.length > 0 ? { @@ -151,11 +174,21 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr } : undefined; + const variableInfo = { + total: variableTotal, + per_workpad: { + avg: variableTotal / variableCounts.length, + min: arrayMin(variableCounts) || 0, + max: arrayMax(variableCounts) || 0, + }, + }; + return { workpads: { total: workpadsInfo.length }, pages: pagesInfo, elements: elementsInfo, functions: functionsInfo, + variables: variableInfo, }; } diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap index 75e132863e47e..c4917539cbeda 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap @@ -6,7 +6,6 @@ exports[` can navigate Autoplay Settings 1`] = ` data-eui="EuiFocusTrap" >
    can navigate Autoplay Settings 2`] = ` data-eui="EuiFocusTrap" >
    can navigate Toolbar Settings, closes when activated 1`] = data-eui="EuiFocusTrap" >
    can navigate Toolbar Settings, closes when activated 2`] = data-eui="EuiFocusTrap" >
    can navigate Toolbar Settings, closes when activated 2`] =
    `; -exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
    Settings
    Hide Toolbar
    Hide the toolbar when the mouse is not within the Canvas?
    "`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
    Settings
    Hide Toolbar
    Hide the toolbar when the mouse is not within the Canvas?
    "`; diff --git a/x-pack/plugins/canvas/storybook/addon/tsconfig.json b/x-pack/plugins/canvas/storybook/addon/tsconfig.json index 9cab0af235f2e..2ab1856de661a 100644 --- a/x-pack/plugins/canvas/storybook/addon/tsconfig.json +++ b/x-pack/plugins/canvas/storybook/addon/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../../tsconfig.base.json", "include": [ "src/**/*.ts", "src/**/*.tsx" diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index c9817de649c25..d8434bd5d9080 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -184,8 +184,6 @@ module.exports = async ({ config: storybookConfig }) => { __dirname, '../tasks/mocks/uiAbsoluteToParsedUrl' ), - ui: path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'), - ng_mock$: path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'), }, }, }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js index eda275ba50c1a..cdb107016e465 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js @@ -11,8 +11,6 @@ jest.mock('../services/auto_follow_pattern_validators', () => ({ validateLeaderIndexPattern: jest.fn(), })); -jest.mock('ui/new_platform'); - describe(' { describe('updateFormErrors()', () => { it('should merge errors with existing fieldsErrors', () => { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js index 93da20a8ed93c..72c4832a631a7 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js @@ -6,8 +6,6 @@ import { updateFields, updateFormErrors } from './follower_index_form'; -jest.mock('ui/new_platform'); - describe(' state transitions', () => { it('updateFormErrors() should merge errors with existing fieldsErrors', () => { const errors = { name: 'Some error' }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js index 11ec125c17d59..924bbe708c73a 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js @@ -6,8 +6,6 @@ import { validateAutoFollowPattern } from './auto_follow_pattern_validators'; -jest.mock('ui/new_platform'); - describe('Auto-follow pattern validators', () => { describe('validateAutoFollowPattern()', () => { it('returns empty object when autoFollowPattern is undefined', () => { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js index 01dccf70a21d6..a3655df1f8956 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js @@ -8,7 +8,6 @@ import { reducer, initialState } from './api'; import { API_STATUS } from '../../constants'; import { apiRequestStart, apiRequestEnd, setApiError } from '../actions'; -jest.mock('ui/new_platform'); jest.mock('../../constants', () => ({ API_STATUS: { IDLE: 'idle', diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index b5e5e248eaeb1..607a11a76a066 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -80,6 +80,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType ), { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 6dfda93db7155..30de62d0d28dd 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -64,6 +64,7 @@ export class FlyoutEditDrilldownAction implements ActionByType ), { diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index fe954f1602cd3..1e2c7987b7041 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -189,7 +189,7 @@ describe('EnhancedSearchInterceptor', () => { const abortController = new AbortController(); abortController.abort(); - const response = searchInterceptor.search({}, { signal: abortController.signal }); + const response = searchInterceptor.search({}, { abortSignal: abortController.signal }); response.subscribe({ next, error }); await timeTravel(500); @@ -225,7 +225,7 @@ describe('EnhancedSearchInterceptor', () => { const response = searchInterceptor.search( {}, - { signal: abortController.signal, pollInterval: 0 } + { abortSignal: abortController.signal, pollInterval: 0 } ); response.subscribe({ next, error }); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 46609af52d072..67a42b9954c9d 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -19,11 +19,10 @@ import { getTotalLoaded, ISearchStrategy, SearchUsage, - ISearchOptions, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; import { shimHitsTotal } from './shim_hits_total'; -import { IEsSearchResponse } from '../../../../../src/plugins/data/common/search/es_search'; +import { ISearchOptions, IEsSearchResponse } from '../../../../../src/plugins/data/common/search'; function isEnhancedEsSearchResponse(response: any): response is IEsSearchResponse { return response.hasOwnProperty('isPartial') && response.hasOwnProperty('isRunning'); diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index 5663671de7bd9..acada946fe0d1 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["embeddable", "uiActionsEnhanced"] + "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"], + "requiredBundles": ["kibanaUtils"] } diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts new file mode 100644 index 0000000000000..a8d5a179dbac1 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/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 * from './url_drilldown'; diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md new file mode 100644 index 0000000000000..996723ccb914d --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md @@ -0,0 +1,24 @@ +# Basic url drilldown implementation + +Url drilldown allows navigating to external URL or to internal kibana URL. +By using variables in url template result url can be dynamic and depend on user's interaction. + +URL drilldown has 3 sources for variables: + +- Global static variables like, for example, `kibanaUrl`. Such variables won’t change depending on a place where url drilldown is used. +- Context variables are dynamic and different depending on where drilldown is created and used. +- Event variables depend on a trigger context. These variables are dynamically extracted from the action context when drilldown is executed. + +Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel), +but `event` variables mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL. + +In current implementation url drilldown has to be used inside the embeddable and with `ValueClickTrigger` or `RangeSelectTrigger`. + +- `context` variables extracted from `embeddable` +- `event` variables extracted from `trigger` context + +In future this basic url drilldown implementation would allow injecting more variables into `context` (e.g. `dashboard` app specific variables) and would allow providing support for new trigger types from outside. +This extensibility improvements are tracked here: https://github.com/elastic/kibana/issues/55324 + +In case a solution app has a use case for url drilldown that has to be different from current basic implementation and +just extending variables list is not enough, then recommendation is to create own custom url drilldown and reuse building blocks from `ui_actions_enhanced`. diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts new file mode 100644 index 0000000000000..748f6f4cecedd --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtUrlDrilldownDisplayName = i18n.translate( + 'xpack.embeddableEnhanced.drilldowns.urlDrilldownDisplayName', + { + defaultMessage: 'Go to URL', + } +); diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts new file mode 100644 index 0000000000000..61406f7d84318 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/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 { UrlDrilldown } from './url_drilldown'; diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts new file mode 100644 index 0000000000000..6a11663ea6c3d --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts @@ -0,0 +1,166 @@ +/* + * 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 { UrlDrilldown, ActionContext, Config } from './url_drilldown'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables'; + +const mockDataPoints = [ + { + table: { + columns: [ + { + name: 'test', + id: '1-1', + meta: { + type: 'histogram', + indexPatternId: 'logstash-*', + aggConfigParams: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, + }, + }, + ], + rows: [ + { + '1-1': '2048', + }, + ], + }, + column: 0, + row: 0, + value: 'test', + }, +]; + +const mockEmbeddable = ({ + getInput: () => ({ + filters: [], + timeRange: { from: 'now-15m', to: 'now' }, + query: { query: 'test', language: 'kuery' }, + }), + getOutput: () => ({}), +} as unknown) as IEmbeddable; + +const mockNavigateToUrl = jest.fn(() => Promise.resolve()); + +describe('UrlDrilldown', () => { + const urlDrilldown = new UrlDrilldown({ + getGlobalScope: () => ({ kibanaUrl: 'http://localhost:5601/' }), + getOpenModal: () => Promise.resolve(coreMock.createStart().overlays.openModal), + getSyntaxHelpDocsLink: () => 'http://localhost:5601/docs', + navigateToUrl: mockNavigateToUrl, + }); + + test('license', () => { + expect(urlDrilldown.minimalLicense).toBe('gold'); + }); + + describe('isCompatible', () => { + test('throws if no embeddable', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + }; + + await expect(urlDrilldown.isCompatible(config, context)).rejects.toThrowError(); + }); + + test('compatible if url is valid', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + embeddable: mockEmbeddable, + }; + + await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(true); + }); + + test('not compatible if url is invalid', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}&{{rison context.panel.somethingFake}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + embeddable: mockEmbeddable, + }; + + await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(false); + }); + }); + + describe('getHref & execute', () => { + beforeEach(() => { + mockNavigateToUrl.mockReset(); + }); + + test('valid url', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + embeddable: mockEmbeddable, + }; + + const url = await urlDrilldown.getHref(config, context); + expect(url).toMatchInlineSnapshot(`"https://elasti.co/?test&(language:kuery,query:test)"`); + + await urlDrilldown.execute(config, context); + expect(mockNavigateToUrl).toBeCalledWith(url); + }); + + test('invalid url', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}&{{rison context.panel.invalid}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + embeddable: mockEmbeddable, + }; + + await expect(urlDrilldown.getHref(config, context)).rejects.toThrowError(); + await expect(urlDrilldown.execute(config, context)).rejects.toThrowError(); + expect(mockNavigateToUrl).not.toBeCalled(); + }); + }); +}); diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx new file mode 100644 index 0000000000000..d5ab095fdd287 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx @@ -0,0 +1,145 @@ +/* + * 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 { OverlayStart } from 'kibana/public'; +import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; +import { ChartActionContext, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../../src/plugins/ui_actions/public'; +import { + UiActionsEnhancedDrilldownDefinition as Drilldown, + UrlDrilldownGlobalScope, + UrlDrilldownConfig, + UrlDrilldownCollectConfig, + urlDrilldownValidateUrlTemplate, + urlDrilldownBuildScope, + urlDrilldownCompileUrl, + UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, +} from '../../../../ui_actions_enhanced/public'; +import { getContextScope, getEventScope, getMockEventScope } from './url_drilldown_scope'; +import { txtUrlDrilldownDisplayName } from './i18n'; + +interface UrlDrilldownDeps { + getGlobalScope: () => UrlDrilldownGlobalScope; + navigateToUrl: (url: string) => Promise; + getOpenModal: () => Promise; + getSyntaxHelpDocsLink: () => string; +} + +export type ActionContext = ChartActionContext; +export type Config = UrlDrilldownConfig; +export type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER; +export interface ActionFactoryContext extends BaseActionFactoryContext { + embeddable?: IEmbeddable; +} +export type CollectConfigProps = CollectConfigPropsBase; + +const URL_DRILLDOWN = 'URL_DRILLDOWN'; + +export class UrlDrilldown implements Drilldown { + public readonly id = URL_DRILLDOWN; + + constructor(private deps: UrlDrilldownDeps) {} + + public readonly order = 8; + + readonly minimalLicense = 'gold'; + readonly licenseFeatureName = 'URL drilldown'; + + public readonly getDisplayName = () => txtUrlDrilldownDisplayName; + + public readonly euiIcon = 'link'; + + supportedTriggers(): UrlTrigger[] { + return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER]; + } + + private readonly ReactCollectConfig: React.FC = ({ + config, + onConfig, + context, + }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const scope = React.useMemo(() => this.buildEditorScope(context), [context]); + return ( + + ); + }; + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + url: { template: '' }, + openInNewTab: false, + }); + + public readonly isConfigValid = ( + config: Config, + context: ActionFactoryContext + ): config is Config => { + const { isValid } = urlDrilldownValidateUrlTemplate(config.url, this.buildEditorScope(context)); + return isValid; + }; + + public readonly isCompatible = async (config: Config, context: ActionContext) => { + const { isValid, error } = urlDrilldownValidateUrlTemplate( + config.url, + await this.buildRuntimeScope(context) + ); + + if (!isValid) { + // eslint-disable-next-line no-console + console.warn( + `UrlDrilldown [${config.url.template}] is not valid. Error [${error}]. Skipping execution.` + ); + } + + return Promise.resolve(isValid); + }; + + public readonly getHref = async (config: Config, context: ActionContext) => + urlDrilldownCompileUrl(config.url.template, await this.buildRuntimeScope(context)); + + public readonly execute = async (config: Config, context: ActionContext) => { + const url = await urlDrilldownCompileUrl( + config.url.template, + await this.buildRuntimeScope(context, { allowPrompts: true }) + ); + if (config.openInNewTab) { + window.open(url, '_blank', 'noopener'); + } else { + await this.deps.navigateToUrl(url); + } + }; + + private buildEditorScope = (context: ActionFactoryContext) => { + return urlDrilldownBuildScope({ + globalScope: this.deps.getGlobalScope(), + contextScope: getContextScope(context), + eventScope: getMockEventScope(context.triggers), + }); + }; + + private buildRuntimeScope = async ( + context: ActionContext, + opts: { allowPrompts: boolean } = { allowPrompts: false } + ) => { + return urlDrilldownBuildScope({ + globalScope: this.deps.getGlobalScope(), + contextScope: getContextScope(context), + eventScope: await getEventScope(context, this.deps, opts), + }); + }; +} diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx new file mode 100644 index 0000000000000..d3e3510f1b24e --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx @@ -0,0 +1,319 @@ +/* + * 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. + */ + +/** + * This file contains all the logic for mapping from trigger's context and action factory context to variables for URL drilldown scope, + * Please refer to ./README.md for explanation of different scope sources + */ + +import React from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiRadioGroup, +} from '@elastic/eui'; +import uniqBy from 'lodash/uniqBy'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/public'; +import { + IEmbeddable, + isRangeSelectTriggerContext, + isValueClickTriggerContext, + RangeSelectContext, + ValueClickContext, +} from '../../../../../../src/plugins/embeddable/public'; +import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown'; +import { SELECT_RANGE_TRIGGER } from '../../../../../../src/plugins/ui_actions/public'; +import { OverlayStart } from '../../../../../../src/core/public'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; + +type ContextScopeInput = ActionContext | ActionFactoryContext; + +/** + * Part of context scope extracted from an embeddable + * Expose on the scope as: `{{context.panel.id}}`, `{{context.panel.filters.[0]}}` + */ +interface EmbeddableUrlDrilldownContextScope { + id: string; + title?: string; + query?: Query; + filters?: Filter[]; + timeRange?: TimeRange; + savedObjectId?: string; + /** + * In case panel supports only 1 index patterns + */ + indexPatternId?: string; + /** + * In case panel supports more then 1 index patterns + */ + indexPatternIds?: string[]; +} + +/** + * Url drilldown context scope + * `{{context.$}}` + */ +interface UrlDrilldownContextScope { + panel?: EmbeddableUrlDrilldownContextScope; +} + +export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilldownContextScope { + function hasEmbeddable(val: unknown): val is { embeddable: IEmbeddable } { + if (val && typeof val === 'object' && 'embeddable' in val) return true; + return false; + } + if (!hasEmbeddable(contextScopeInput)) + throw new Error( + "UrlDrilldown [getContextScope] can't build scope because embeddable object is missing in context" + ); + + const embeddable = contextScopeInput.embeddable; + const input = embeddable.getInput(); + const output = embeddable.getOutput(); + function hasSavedObjectId(obj: Record): obj is { savedObjectId: string } { + return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string'; + } + function getIndexPatternIds(): string[] { + function hasIndexPatterns( + _output: Record + ): _output is { indexPatterns: Array<{ id?: string }> } { + return ( + 'indexPatterns' in _output && + Array.isArray(_output.indexPatterns) && + _output.indexPatterns.length > 0 + ); + } + return hasIndexPatterns(output) + ? (output.indexPatterns.map((ip) => ip.id).filter(Boolean) as string[]) + : []; + } + const indexPatternsIds = getIndexPatternIds(); + return { + panel: cleanEmptyKeys({ + id: input.id, + title: output.title ?? input.title, + savedObjectId: + output.savedObjectId ?? (hasSavedObjectId(input) ? input.savedObjectId : undefined), + query: input.query, + timeRange: input.timeRange, + filters: input.filters, + indexPatternIds: indexPatternsIds.length > 1 ? indexPatternsIds : undefined, + indexPatternId: indexPatternsIds.length === 1 ? indexPatternsIds[0] : undefined, + }), + }; +} + +/** + * URL drilldown event scope, + * available as: {{event.key}}, {{event.from}} + */ +type UrlDrilldownEventScope = ValueClickTriggerEventScope | RangeSelectTriggerEventScope; +type EventScopeInput = ActionContext; +interface ValueClickTriggerEventScope { + key?: string; + value?: string | number | boolean; + negate: boolean; +} +interface RangeSelectTriggerEventScope { + key: string; + from?: string | number; + to?: string | number; +} + +export async function getEventScope( + eventScopeInput: EventScopeInput, + deps: { getOpenModal: () => Promise }, + opts: { allowPrompts: boolean } = { allowPrompts: false } +): Promise { + if (isRangeSelectTriggerContext(eventScopeInput)) { + return getEventScopeFromRangeSelectTriggerContext(eventScopeInput); + } else if (isValueClickTriggerContext(eventScopeInput)) { + return getEventScopeFromValueClickTriggerContext(eventScopeInput, deps, opts); + } else { + throw new Error("UrlDrilldown [getEventScope] can't build scope from not supported trigger"); + } +} + +async function getEventScopeFromRangeSelectTriggerContext( + eventScopeInput: RangeSelectContext +): Promise { + const { table, column: columnIndex, range } = eventScopeInput.data; + const column = table.columns[columnIndex]; + return cleanEmptyKeys({ + key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string, + from: toPrimitiveOrUndefined(range[0]) as string | number | undefined, + to: toPrimitiveOrUndefined(range[range.length - 1]) as string | number | undefined, + }); +} + +async function getEventScopeFromValueClickTriggerContext( + eventScopeInput: ValueClickContext, + deps: { getOpenModal: () => Promise }, + opts: { allowPrompts: boolean } = { allowPrompts: false } +): Promise { + const negate = eventScopeInput.data.negate ?? false; + const point = await getSingleValue(eventScopeInput.data.data, deps, opts); + const { key, value } = getKeyValueFromPoint(point); + return cleanEmptyKeys({ + key, + value, + negate, + }); +} + +/** + * @remarks + * Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel) + * `event` variables are mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL + */ +export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventScope { + if (trigger === SELECT_RANGE_TRIGGER) { + return { + key: 'event.key', + from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago + to: new Date().toISOString(), + }; + } else { + return { + key: 'event.key', + value: 'event.value', + negate: false, + }; + } +} + +function getKeyValueFromPoint( + point: ValueClickContext['data']['data'][0] +): Pick { + const { table, column: columnIndex, value } = point; + const column = table.columns[columnIndex]; + return { + key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string | undefined, + value: toPrimitiveOrUndefined(value), + }; +} + +function toPrimitiveOrUndefined(v: unknown): string | number | boolean | undefined { + if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'string') return v; + if (typeof v === 'object' && v instanceof Date) return v.toISOString(); + if (typeof v === 'undefined' || v === null) return undefined; + return String(v); +} + +function cleanEmptyKeys>(obj: T): T { + Object.keys(obj).forEach((key) => { + if (obj[key] === undefined) { + delete obj[key]; + } + }); + return obj; +} + +/** + * VALUE_CLICK_TRIGGER could have multiple data points + * Prompt user which data point to use in a drilldown + */ +async function getSingleValue( + data: ValueClickContext['data']['data'], + deps: { getOpenModal: () => Promise }, + opts: { allowPrompts: boolean } = { allowPrompts: false } +): Promise { + data = uniqBy(data.filter(Boolean), (point) => { + const { key, value } = getKeyValueFromPoint(point); + return `${key}:${value}`; + }); + if (data.length === 0) + throw new Error(`[trigger = "VALUE_CLICK_TRIGGER"][getSingleValue] no value to pick from`); + if (data.length === 1) return Promise.resolve(data[0]); + if (!opts.allowPrompts) return Promise.resolve(data[0]); + return new Promise(async (resolve, reject) => { + const openModal = await deps.getOpenModal(); + const overlay = openModal( + toMountPoint( + overlay.close()} + onSubmit={(point) => { + if (point) { + resolve(point); + } + overlay.close(); + }} + data={data} + /> + ) + ); + overlay.onClose.then(() => reject()); + }); +} + +function GetSingleValuePopup({ + data, + onCancel, + onSubmit, +}: { + data: ValueClickContext['data']['data']; + onCancel: () => void; + onSubmit: (value: ValueClickContext['data']['data'][0]) => void; +}) { + const values = data + .map((point) => { + const { key, value } = getKeyValueFromPoint(point); + return { + point, + id: key ?? '', + label: `${key}:${value}`, + }; + }) + .filter((value) => Boolean(value.id)); + + const [selectedValueId, setSelectedValueId] = React.useState(values[0].id); + + return ( + + + + + + + + + setSelectedValueId(id)} + name="drilldownValues" + /> + + + + + + + onSubmit(values.find((v) => v.id === selectedValueId)?.point!)} + data-test-subj="applySingleValuePopoverButton" + fill + > + + + + + ); +} diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index fd0bcc2023269..37e102b40131d 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -28,8 +28,11 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsSetup, AdvancedUiActionsStart, + urlDrilldownGlobalScopeProvider, } from '../../ui_actions_enhanced/public'; import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions'; +import { UrlDrilldown } from './drilldowns'; +import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -61,11 +64,21 @@ export class EmbeddableEnhancedPlugin public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { this.setCustomEmbeddableFactoryProvider(plugins); - + const startServices = createStartServicesGetter(core.getStartServices); const panelNotificationAction = new PanelNotificationsAction(); plugins.uiActionsEnhanced.registerAction(panelNotificationAction); plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); + plugins.uiActionsEnhanced.registerDrilldown( + new UrlDrilldown({ + getGlobalScope: urlDrilldownGlobalScopeProvider({ core }), + navigateToUrl: (url: string) => + core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)), + getOpenModal: () => core.getStartServices().then(([{ overlays }]) => overlays.openModal), + getSyntaxHelpDocsLink: () => startServices().core.docLinks.links.dashboard.drilldowns, // TODO: replace with docs https://github.com/elastic/kibana/issues/69414 + }) + ); + return {}; } diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index c5839df4c603b..05d27d7337a6e 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -24,6 +24,7 @@ export const APP_SEARCH_PLUGIN = { 'Leverage dashboards, analytics, and APIs for advanced application search made simple.', }), URL: '/app/enterprise_search/app_search', + SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/app-search/', }; export const WORKPLACE_SEARCH_PLUGIN = { @@ -36,8 +37,11 @@ export const WORKPLACE_SEARCH_PLUGIN = { 'Search all documents, files, and sources available across your virtual workplace.', }), URL: '/app/enterprise_search/workplace_search', + SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/workplace-search/', }; +export const LICENSED_SUPPORT_URL = 'https://support.elastic.co'; + export const JSON_HEADER = { 'Content-Type': 'application/json' }; // This needs specific casing or Chrome throws a 415 error export const ENGINES_PAGE_SIZE = 10; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 0fb3bb8080d82..3f71759390879 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -4,28 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { IInitialAppData } from '../../../common/types'; -import { IKeaLogic } from '../shared/types'; -export interface IAppLogicValues { +export interface IAppValues { hasInitialized: boolean; } -export interface IAppLogicActions { +export interface IAppActions { initializeAppData(props: IInitialAppData): void; } -export const AppLogic = kea({ - actions: (): IAppLogicActions => ({ +export const AppLogic = kea>({ + actions: { initializeAppData: (props) => props, - }), - reducers: () => ({ + }, + reducers: { hasInitialized: [ false, { initializeAppData: () => true, }, ], - }), -}) as IKeaLogic; + }, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg deleted file mode 100644 index ceab918e92e70..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg deleted file mode 100644 index 2284a425b5add..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg deleted file mode 100644 index 4e01e9a0b34fb..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/engine_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/engine_icon.tsx new file mode 100644 index 0000000000000..64494bfaa7949 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/engine_icon.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +export const EngineIcon: React.FC = () => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/meta_engine_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/meta_engine_icon.tsx new file mode 100644 index 0000000000000..e2939ddcde9d7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/assets/meta_engine_icon.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +export const MetaEngineIcon: React.FC = () => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss index e39bbbc95564b..5e8a20ba425ad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss @@ -15,6 +15,8 @@ .engineIcon { display: inline-block; width: $euiSize; - height: $euiSize; + // Use line-height of EuiTitle - SVG will vertically center accordingly + height: map-get(map-get($euiTitles, 's'), 'line-height'); + vertical-align: top; margin-right: $euiSizeXS; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index c3b47b2b585bd..9703fde7e140a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -20,8 +20,8 @@ import { FlashMessages } from '../../../shared/flash_messages'; import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing'; import { KibanaContext, IKibanaContext } from '../../../index'; -import EnginesIcon from '../../assets/engine.svg'; -import MetaEnginesIcon from '../../assets/meta_engine.svg'; +import { EngineIcon } from './assets/engine_icon'; +import { MetaEngineIcon } from './assets/meta_engine_icon'; import { EngineOverviewHeader, LoadingState, EmptyState } from './components'; import { EngineTable } from './engine_table'; @@ -93,7 +93,7 @@ export const EngineOverview: React.FC = () => {

    - + {

    - + ( = (props) => { @@ -48,9 +49,9 @@ export const AppSearchUnconfigured: React.FC = () => ( ); export const AppSearchConfigured: React.FC = (props) => { - const { hasInitialized } = useValues(AppLogic) as IAppLogicValues; - const { initializeAppData } = useActions(AppLogic) as IAppLogicActions; - const { errorConnecting } = useValues(HttpLogic) as IHttpLogicValues; + const { hasInitialized } = useValues(AppLogic); + const { initializeAppData } = useActions(AppLogic); + const { errorConnecting } = useValues(HttpLogic); useEffect(() => { if (!hasInitialized) initializeAppData(props); @@ -73,6 +74,9 @@ export const AppSearchConfigured: React.FC = (props) => { + + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/kea.d.ts b/x-pack/plugins/enterprise_search/public/applications/kea.d.ts deleted file mode 100644 index 961d93ccc12e6..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/kea.d.ts +++ /dev/null @@ -1,13 +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. - */ - -declare module 'kea' { - export function useValues(logic?: object): object; - export function useActions(logic?: object): object; - export function getContext(): { store: object }; - export function resetContext(context: object): object; - export function kea(logic: object): object; -} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx index 5a909a287795c..d184eb4dcd644 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { useValues } from 'kea'; import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui'; -import { FlashMessagesLogic, IFlashMessagesValues } from './flash_messages_logic'; +import { FlashMessagesLogic } from './flash_messages_logic'; const FLASH_MESSAGE_TYPES = { success: { color: 'success' as EuiCallOutProps['color'], icon: 'check' }, @@ -18,7 +18,7 @@ const FLASH_MESSAGE_TYPES = { }; export const FlashMessages: React.FC = ({ children }) => { - const { messages } = useValues(FlashMessagesLogic) as IFlashMessagesValues; + const { messages } = useValues(FlashMessagesLogic); // If we have no messages to display, do not render the element at all if (!messages.length) return null; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 96c7817832c52..3ae48f352b2c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { ReactNode } from 'react'; import { History } from 'history'; -import { IKeaLogic, TKeaReducers, IKeaParams } from '../types'; - export interface IFlashMessage { type: 'success' | 'info' | 'warning' | 'error'; message: ReactNode; @@ -22,27 +20,27 @@ export interface IFlashMessagesValues { historyListener: Function | null; } export interface IFlashMessagesActions { - setFlashMessages(messages: IFlashMessage | IFlashMessage[]): void; + setFlashMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; clearFlashMessages(): void; - setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): void; + setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; clearQueuedMessages(): void; - listenToHistory(history: History): void; - setHistoryListener(historyListener: Function): void; + listenToHistory(history: History): History; + setHistoryListener(historyListener: Function): { historyListener: Function }; } const convertToArray = (messages: IFlashMessage | IFlashMessage[]) => !Array.isArray(messages) ? [messages] : messages; -export const FlashMessagesLogic = kea({ - actions: (): IFlashMessagesActions => ({ +export const FlashMessagesLogic = kea>({ + actions: { setFlashMessages: (messages) => ({ messages: convertToArray(messages) }), clearFlashMessages: () => null, setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }), clearQueuedMessages: () => null, listenToHistory: (history) => history, setHistoryListener: (historyListener) => ({ historyListener }), - }), - reducers: (): TKeaReducers => ({ + }, + reducers: { messages: [ [], { @@ -63,8 +61,8 @@ export const FlashMessagesLogic = kea({ setHistoryListener: (_, { historyListener }) => historyListener, }, ], - }), - listeners: ({ values, actions }): Partial => ({ + }, + listeners: ({ values, actions }) => ({ listenToHistory: (history) => { // On React Router navigation, clear previous flash messages and load any queued messages const unlisten = history.listen(() => { @@ -81,7 +79,4 @@ export const FlashMessagesLogic = kea({ if (removeHistoryListener) removeHistoryListener(); }, }), -} as IKeaParams) as IKeaLogic< - IFlashMessagesValues, - IFlashMessagesActions ->; +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx index 584124468a91f..a3ceabcf6ac8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx @@ -8,19 +8,15 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; import { History } from 'history'; -import { - FlashMessagesLogic, - IFlashMessagesValues, - IFlashMessagesActions, -} from './flash_messages_logic'; +import { FlashMessagesLogic } from './flash_messages_logic'; interface IFlashMessagesProviderProps { history: History; } export const FlashMessagesProvider: React.FC = ({ history }) => { - const { historyListener } = useValues(FlashMessagesLogic) as IFlashMessagesValues; - const { listenToHistory } = useActions(FlashMessagesLogic) as IFlashMessagesActions; + const { historyListener } = useValues(FlashMessagesLogic); + const { listenToHistory } = useActions(FlashMessagesLogic); useEffect(() => { if (!historyListener) listenToHistory(history); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts index 7bf7a19ed451f..fb2d9b1061723 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -4,32 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { HttpSetup } from 'src/core/public'; -import { IKeaLogic, IKeaParams, TKeaReducers } from '../../shared/types'; - -export interface IHttpLogicValues { +export interface IHttpValues { http: HttpSetup; httpInterceptors: Function[]; errorConnecting: boolean; } -export interface IHttpLogicActions { - initializeHttp({ http, errorConnecting }: { http: HttpSetup; errorConnecting?: boolean }): void; +export interface IHttpActions { + initializeHttp({ + http, + errorConnecting, + }: { + http: HttpSetup; + errorConnecting?: boolean; + }): { http: HttpSetup; errorConnecting?: boolean }; initializeHttpInterceptors(): void; - setHttpInterceptors(httpInterceptors: Function[]): void; - setErrorConnecting(errorConnecting: boolean): void; + setHttpInterceptors(httpInterceptors: Function[]): { httpInterceptors: Function[] }; + setErrorConnecting(errorConnecting: boolean): { errorConnecting: boolean }; } -export const HttpLogic = kea({ - actions: (): IHttpLogicActions => ({ +export const HttpLogic = kea>({ + actions: { initializeHttp: ({ http, errorConnecting }) => ({ http, errorConnecting }), initializeHttpInterceptors: () => null, setHttpInterceptors: (httpInterceptors) => ({ httpInterceptors }), setErrorConnecting: (errorConnecting) => ({ errorConnecting }), - }), - reducers: (): TKeaReducers => ({ + }, + reducers: { http: [ (null as unknown) as HttpSetup, { @@ -49,7 +53,7 @@ export const HttpLogic = kea({ setErrorConnecting: (_, { errorConnecting }) => errorConnecting, }, ], - }), + }, listeners: ({ values, actions }) => ({ initializeHttpInterceptors: () => { const httpInterceptors = []; @@ -80,7 +84,4 @@ export const HttpLogic = kea({ }); }, }), -} as IKeaParams) as IKeaLogic< - IHttpLogicValues, - IHttpLogicActions ->; +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx index 6febc1869054f..4c2160195a1af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx @@ -9,7 +9,7 @@ import { useActions } from 'kea'; import { HttpSetup } from 'src/core/public'; -import { HttpLogic, IHttpLogicActions } from './http_logic'; +import { HttpLogic } from './http_logic'; interface IHttpProviderProps { http: HttpSetup; @@ -17,7 +17,7 @@ interface IHttpProviderProps { } export const HttpProvider: React.FC = (props) => { - const { initializeHttp, initializeHttpInterceptors } = useActions(HttpLogic) as IHttpLogicActions; + const { initializeHttp, initializeHttpInterceptors } = useActions(HttpLogic); useEffect(() => { initializeHttp(props); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts index 449ff9d56debf..db65e80ca25c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { HttpLogic, IHttpLogicValues, IHttpLogicActions } from './http_logic'; +export { HttpLogic, IHttpValues, IHttpActions } from './http_logic'; export { HttpProvider } from './http_provider'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts index 9c8c1417d48db..29c11ffa1cef8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts @@ -5,4 +5,4 @@ */ export { LicenseContext, LicenseProvider, ILicenseContext } from './license_context'; -export { hasPlatinumLicense } from './license_checks'; +export { hasPlatinumLicense, hasGoldLicense } from './license_checks'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts index ad134e7d36b10..40f0f6380c21c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { hasPlatinumLicense } from './license_checks'; +import { hasPlatinumLicense, hasGoldLicense } from './license_checks'; describe('hasPlatinumLicense', () => { it('is true for platinum licenses', () => { @@ -31,3 +31,24 @@ describe('hasPlatinumLicense', () => { expect(hasPlatinumLicense({ isActive: true, type: 'gold' } as any)).toEqual(false); }); }); + +describe('hasGoldLicense', () => { + it('is true for gold+ and trial licenses', () => { + expect(hasGoldLicense({ isActive: true, type: 'gold' } as any)).toEqual(true); + expect(hasGoldLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true); + expect(hasGoldLicense({ isActive: true, type: 'enterprise' } as any)).toEqual(true); + expect(hasGoldLicense({ isActive: true, type: 'trial' } as any)).toEqual(true); + }); + + it('is false if the current license is expired', () => { + expect(hasGoldLicense({ isActive: false, type: 'gold' } as any)).toEqual(false); + expect(hasGoldLicense({ isActive: false, type: 'platinum' } as any)).toEqual(false); + expect(hasGoldLicense({ isActive: false, type: 'enterprise' } as any)).toEqual(false); + expect(hasGoldLicense({ isActive: false, type: 'trial' } as any)).toEqual(false); + }); + + it('is false for licenses below gold', () => { + expect(hasGoldLicense({ isActive: true, type: 'basic' } as any)).toEqual(false); + expect(hasGoldLicense({ isActive: false, type: 'standard' } as any)).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts index de4a17ce2bd3c..d13d0909243be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts @@ -7,5 +7,11 @@ import { ILicense } from '../../../../../licensing/public'; export const hasPlatinumLicense = (license?: ILicense) => { - return license?.isActive && ['platinum', 'enterprise', 'trial'].includes(license?.type as string); + const qualifyingLicenses = ['platinum', 'enterprise', 'trial']; + return license?.isActive && qualifyingLicenses.includes(license?.type as string); +}; + +export const hasGoldLicense = (license?: ILicense) => { + const qualifyingLicenses = ['gold', 'platinum', 'enterprise', 'trial']; + return license?.isActive && qualifyingLicenses.includes(license?.type as string); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx new file mode 100644 index 0000000000000..6bd0486532576 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx @@ -0,0 +1,32 @@ +/* + * 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'; + +export const AppSearchLogo: React.FC = () => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss new file mode 100644 index 0000000000000..322e6a4182c5c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss @@ -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. + */ + +.logo404 { + width: $euiSize * 8; + height: $euiSize * 8; + + fill: $euiColorEmptyShade; + stroke: $euiColorLightShade; + + &__light { + fill: $euiColorLightShade; + } + + &__dark { + fill: $euiColorMediumShade; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx new file mode 100644 index 0000000000000..30bcf9ad2d7fd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx @@ -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 React from 'react'; + +export const WorkplaceSearchLogo: React.FC = () => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts new file mode 100644 index 0000000000000..dcc7762b93214 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/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 { NotFound } from './not_found'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx new file mode 100644 index 0000000000000..ce9071ad7b9d0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx @@ -0,0 +1,65 @@ +/* + * 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 '../../__mocks__/shallow_usecontext.mock'; + +import React, { useContext } from 'react'; +import { shallow } from 'enzyme'; + +import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui'; + +import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants'; +import { AppSearchLogo } from './assets/app_search_logo'; +import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; + +import { NotFound } from './'; + +describe('NotFound', () => { + const basicLicense = { isActive: true, type: 'basic' }; + const goldLicense = { isActive: true, type: 'gold' }; + + beforeEach(() => { + (useContext as jest.Mock).mockImplementation(() => ({ license: basicLicense })); + }); + + it('renders an App Search 404 view', () => { + const wrapper = shallow(); + const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); + + expect(prompt.find('h2').text()).toEqual('404 error'); + expect(prompt.find(EuiButtonExternal).prop('href')).toEqual(APP_SEARCH_PLUGIN.SUPPORT_URL); + + const logo = prompt.find(AppSearchLogo).dive().shallow(); + expect(logo.type()).toEqual('svg'); + }); + + it('renders a Workplace Search 404 view', () => { + const wrapper = shallow(); + const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); + + expect(prompt.find('h2').text()).toEqual('404 error'); + expect(prompt.find(EuiButtonExternal).prop('href')).toEqual( + WORKPLACE_SEARCH_PLUGIN.SUPPORT_URL + ); + + const logo = prompt.find(WorkplaceSearchLogo).dive().shallow(); + expect(logo.type()).toEqual('svg'); + }); + + it('changes the support URL if the user has a gold+ license', () => { + (useContext as jest.Mock).mockImplementation(() => ({ license: goldLicense })); + const wrapper = shallow(); + const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); + + expect(prompt.find(EuiButtonExternal).prop('href')).toEqual('https://support.elastic.co'); + }); + + it('does not render anything without a valid product', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx new file mode 100644 index 0000000000000..bd988854225fb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPageContent, + EuiEmptyPrompt, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButton as EuiButtonExternal, +} from '@elastic/eui'; + +import { + APP_SEARCH_PLUGIN, + WORKPLACE_SEARCH_PLUGIN, + LICENSED_SUPPORT_URL, +} from '../../../../common/constants'; + +import { EuiButton } from '../react_router_helpers'; +import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; +import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; +import { LicenseContext, ILicenseContext, hasGoldLicense } from '../licensing'; + +import { AppSearchLogo } from './assets/app_search_logo'; +import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; +import './assets/logo.scss'; + +interface NotFoundProps { + // Expects product plugin constants (@see common/constants.ts) + product: { + ID: string; + SUPPORT_URL: string; + }; +} + +export const NotFound: React.FC = ({ product = {} }) => { + const { license } = useContext(LicenseContext) as ILicenseContext; + const supportUrl = hasGoldLicense(license) ? LICENSED_SUPPORT_URL : product.SUPPORT_URL; + + let Logo; + let SetPageChrome; + let SendTelemetry; + + switch (product.ID) { + case APP_SEARCH_PLUGIN.ID: + Logo = AppSearchLogo; + SetPageChrome = SetAppSearchChrome; + SendTelemetry = SendAppSearchTelemetry; + break; + case WORKPLACE_SEARCH_PLUGIN.ID: + Logo = WorkplaceSearchLogo; + SetPageChrome = SetWorkplaceSearchChrome; + SendTelemetry = SendWorkplaceSearchTelemetry; + break; + default: + return null; + } + + return ( + <> + + + + + } + body={ + <> + +

    + {i18n.translate('xpack.enterpriseSearch.notFound.title', { + defaultMessage: '404 error', + })} +

    +
    +

    + {i18n.translate('xpack.enterpriseSearch.notFound.description', { + defaultMessage: 'The page you’re looking for was not found.', + })} +

    + + } + actions={ + + + + {i18n.translate('xpack.enterpriseSearch.notFound.action1', { + defaultMessage: 'Back to your dashboard', + })} + + + + + {i18n.translate('xpack.enterpriseSearch.notFound.action2', { + defaultMessage: 'Contact support', + })} + + + + } + /> +
    + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 561016d36921d..3fd1dcad0066e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -5,63 +5,3 @@ */ export { IFlashMessage } from './flash_messages'; - -export interface IKeaLogic { - mount(): Function; - values: IKeaValues; - actions: IKeaActions; -} - -/** - * This reusable interface mostly saves us a few characters / allows us to skip - * defining params inline. Unfortunately, the return values *do not work* as - * expected (hence the voids). While I can tell selectors to use TKeaSelectors, - * the return value is *not* properly type checked if it's not declared inline. :/ - * - * Also note that if you switch to Kea 2.1's plain object notation - - * `selectors: {}` vs. `selectors: () => ({})` - * - type checking also stops working and type errors become significantly less - * helpful - showing less specific error messages and highlighting. 👎 - */ -export interface IKeaParams { - selectors?(params: { selectors: IKeaValues }): void; - listeners?(params: { actions: IKeaActions; values: IKeaValues }): void; - events?(params: { actions: IKeaActions; values: IKeaValues }): void; -} - -/** - * This reducers() type checks that: - * - * 1. The value object keys are defined within IKeaValues - * 2. The default state (array[0]) matches the type definition within IKeaValues - * 3. The action object keys (array[1]) are defined within IKeaActions - * 3. The new state returned by the action matches the type definition within IKeaValues - */ -export type TKeaReducers = { - [Value in keyof IKeaValues]?: [ - IKeaValues[Value], - { - [Action in keyof IKeaActions]?: ( - state: IKeaValues[Value], - payload: IKeaValues - ) => IKeaValues[Value]; - } - ]; -}; - -/** - * This selectors() type checks that: - * - * 1. The object keys are defined within IKeaValues - * 2. The selected values are defined within IKeaValues - * 3. The returned value match the type definition within IKeaValues - * - * The unknown[] and any[] are unfortunately because I have no idea how to - * assert for arbitrary type/values as an array - */ -export type TKeaSelectors = { - [Value in keyof IKeaValues]?: [ - (selectors: IKeaValues) => unknown[], - (...args: any[]) => IKeaValues[Value] // eslint-disable-line @typescript-eslint/no-explicit-any - ]; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts new file mode 100644 index 0000000000000..7789d0caba345 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const contentSources = [ + { + id: '123', + serviceType: 'custom', + searchable: true, + supportedByLicense: true, + status: 'foo', + statusMessage: 'bar', + name: 'source', + documentCount: '123', + isFederatedSource: false, + errorReason: 0, + allowsReauth: true, + boost: 1, + }, + { + id: '123', + serviceType: 'jira', + searchable: true, + supportedByLicense: true, + status: 'synced', + statusMessage: 'all green', + name: 'Jira', + documentCount: '34234', + isFederatedSource: false, + errorReason: 0, + allowsReauth: true, + boost: 0.5, + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/users.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/users.mock.ts new file mode 100644 index 0000000000000..0294803042ada --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/users.mock.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. + */ + +export const users = [ + { + id: '1z1z', + name: 'John Does', + pictureUrl: 'http://google.cats', + color: '#ededed', + initials: 'JD', + email: 'jd@elastic.co', + groupIds: [], + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index b7116f02663c1..5bf2b41cfc264 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { IInitialAppData } from '../../../common/types'; import { IWorkplaceSearchInitialData } from '../../../common/types/workplace_search'; -import { IKeaLogic } from '../shared/types'; export interface IAppValues extends IWorkplaceSearchInitialData { hasInitialized: boolean; @@ -17,16 +16,16 @@ export interface IAppActions { initializeAppData(props: IInitialAppData): void; } -export const AppLogic = kea({ - actions: (): IAppActions => ({ +export const AppLogic = kea>({ + actions: { initializeAppData: ({ workplaceSearch }) => workplaceSearch, - }), - reducers: () => ({ + }, + reducers: { hasInitialized: [ false, { initializeAppData: () => true, }, ], - }), -}) as IKeaLogic; + }, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/logo.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/logo.svg deleted file mode 100644 index e6b987c398268..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/logo.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg new file mode 100644 index 0000000000000..1e7324d9581a7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/confluence.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/confluence.svg new file mode 100644 index 0000000000000..23eff13915401 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/confluence.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/connection_illustration.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/connection_illustration.svg new file mode 100644 index 0000000000000..c1c3c3b3ee9ee --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/connection_illustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/crawler.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/crawler.svg new file mode 100644 index 0000000000000..d241989f1aff1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/crawler.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/custom.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/custom.svg new file mode 100644 index 0000000000000..f8f6415dea22b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/custom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/drive.svg new file mode 100644 index 0000000000000..40b65df3a1ce3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/dropbox.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/dropbox.svg new file mode 100644 index 0000000000000..d16f293fde6dc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/dropbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/github.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/github.svg new file mode 100644 index 0000000000000..c4b4176560d5b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/gmail.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/gmail.svg new file mode 100644 index 0000000000000..ee824f730aca6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/gmail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google.svg new file mode 100644 index 0000000000000..22630f533dcbf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google_drive.svg new file mode 100644 index 0000000000000..59469d814e35f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google_drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/index.ts new file mode 100644 index 0000000000000..5f93694da09b8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/index.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 box from './box.svg'; +import confluence from './confluence.svg'; +import crawler from './crawler.svg'; +import custom from './custom.svg'; +import drive from './drive.svg'; +import dropbox from './dropbox.svg'; +import github from './github.svg'; +import gmail from './gmail.svg'; +import google from './google.svg'; +import googleDrive from './google_drive.svg'; +import jira from './jira.svg'; +import jiraServer from './jira_server.svg'; +import loadingSmall from './loading_small.svg'; +import office365 from './office365.svg'; +import oneDrive from './one_drive.svg'; +import outlook from './outlook.svg'; +import people from './people.svg'; +import salesforce from './salesforce.svg'; +import serviceNow from './service_now.svg'; +import sharePoint from './share_point.svg'; +import slack from './slack.svg'; +import zendesk from './zendesk.svg'; + +export const images = { + box, + confluence, + crawler, + custom, + drive, + dropbox, + github, + gmail, + googleDrive, + google, + jira, + jiraServer, + loadingSmall, + office365, + oneDrive, + outlook, + people, + salesforce, + serviceNow, + sharePoint, + slack, + zendesk, +} as { [key: string]: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira.svg new file mode 100644 index 0000000000000..224bb822a581c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira_server.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira_server.svg new file mode 100644 index 0000000000000..71750fb6e25a0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira_server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/loading_small.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/loading_small.svg new file mode 100644 index 0000000000000..159408ea02938 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/loading_small.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/office365.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/office365.svg new file mode 100644 index 0000000000000..fdce5d02da3cd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/office365.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/one_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/one_drive.svg new file mode 100644 index 0000000000000..1856e5e3ce1af --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/one_drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/outlook.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/outlook.svg new file mode 100644 index 0000000000000..2680bc99cc367 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/outlook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/people.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/people.svg new file mode 100644 index 0000000000000..4500c494c23b7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/people.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/salesforce.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/salesforce.svg new file mode 100644 index 0000000000000..510c438a28195 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/salesforce.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/service_now.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/service_now.svg new file mode 100644 index 0000000000000..2d0c09db4e1c3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/service_now.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_point.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_point.svg new file mode 100644 index 0000000000000..8724be9da88cf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_point.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/slack.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/slack.svg new file mode 100644 index 0000000000000..14dbd0289da84 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/zendesk.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/zendesk.svg new file mode 100644 index 0000000000000..f7bc1fda0c9ac --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/zendesk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/index.ts new file mode 100644 index 0000000000000..da0d11cc1fdc5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/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 { SourceIcon } from './source_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx new file mode 100644 index 0000000000000..dd37ba9b6d859 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx @@ -0,0 +1,27 @@ +/* + * 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 '../../../../__mocks__/shallow_usecontext.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SourceIcon } from './'; + +describe('SourceIcon', () => { + it('renders unwrapped icon', () => { + const wrapper = shallow(); + + expect(wrapper.find('img')).toHaveLength(1); + expect(wrapper.find('.user-group-source')).toHaveLength(0); + }); + + it('renders wrapped icon', () => { + const wrapper = shallow(); + + expect(wrapper.find('.user-group-source')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx new file mode 100644 index 0000000000000..ddbe327a40a30 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx @@ -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 React from 'react'; + +import _camelCase from 'lodash/camelCase'; + +import { images } from '../assets'; + +interface ISourceIconProps { + serviceType: string; + name: string; + className?: string; + wrapped?: boolean; +} + +export const SourceIcon: React.FC = ({ + name, + serviceType, + className, + wrapped, +}) => { + const icon = {name}; + return wrapped ? ( +
    + {icon} +
    + ) : ( + <>{icon} + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/index.ts new file mode 100644 index 0000000000000..f7a99b59837a5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/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 { SourceRow, ISourceRow } from './source_row'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx new file mode 100644 index 0000000000000..2bde3b70f82b5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx @@ -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 '../../../../__mocks__/shallow_usecontext.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiTableRow, EuiSwitch, EuiIcon } from '@elastic/eui'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import { SourceIcon } from '../source_icon'; + +import { SourceRow } from './'; + +const onToggle = jest.fn(); + +describe('SourceRow', () => { + it('renders with no "Fix" link', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + expect(wrapper.contains('Fix')).toBeFalsy(); + expect(wrapper.find(SourceIcon).prop('serviceType')).toEqual('custom'); + }); + + it('calls handler on click', () => { + const wrapper = shallow(); + wrapper.find(EuiSwitch).simulate('change', { target: { checked: true } }); + + expect(onToggle).toHaveBeenCalled(); + }); + + it('renders "Fix" link', () => { + const source = { + ...contentSources[0], + status: 'error', + errorReason: 1, + }; + const wrapper = shallow(); + + expect(wrapper.contains('Fix')).toBeTruthy(); + }); + + it('renders loading icon when indexing', () => { + const source = { + ...contentSources[0], + status: 'indexing', + }; + const wrapper = shallow(); + + expect(wrapper.find(SourceIcon).prop('serviceType')).toEqual('loadingSmall'); + }); + + it('renders warning dot when more config needed', () => { + const source = { + ...contentSources[0], + status: 'need-more-config', + }; + const wrapper = shallow(); + + expect(wrapper.find(EuiIcon).prop('color')).toEqual('warning'); + }); + + it('renders remote tooltip when source is federated', () => { + const source = { + ...contentSources[0], + isFederatedSource: true, + }; + const wrapper = shallow(); + + expect(wrapper.find('.source-row__document-count').contains('Remote')).toBeTruthy(); + }); + + it('renders details link', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="SourceDetailsLink"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx new file mode 100644 index 0000000000000..17ca8e58a80fa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx @@ -0,0 +1,171 @@ +/* + * 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 classNames from 'classnames'; +import _kebabCase from 'lodash/kebabCase'; +import { Link } from 'react-router-dom'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSwitch, + EuiSwitchEvent, + EuiTableRow, + EuiTableRowCell, + EuiText, + EuiToolTip, +} from '@elastic/eui'; + +import { SOURCE_STATUSES as statuses } from '../../../constants'; +import { IContentSourceDetails } from '../../../types'; +import { ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, getContentSourcePath } from '../../../routes'; + +import { SourceIcon } from '../source_icon'; + +const CREDENTIALS_INVALID_ERROR_REASON = 1; + +export interface ISourceRow { + showDetails?: boolean; + isOrganization?: boolean; + onSearchableToggle?(sourceId: string, isSearchable: boolean): void; +} + +interface ISourceRowProps extends ISourceRow { + source: IContentSourceDetails; +} + +export const SourceRow: React.FC = ({ + source: { + id, + serviceType, + searchable, + supportedByLicense, + status, + statusMessage, + name, + documentCount, + isFederatedSource, + errorReason, + allowsReauth, + }, + onSearchableToggle, + isOrganization, + showDetails, +}) => { + const isIndexing = status === statuses.INDEXING; + const hasError = status === statuses.ERROR || status === statuses.DISCONNECTED; + const showFix = + isOrganization && hasError && allowsReauth && errorReason === CREDENTIALS_INVALID_ERROR_REASON; + + const rowClass = classNames( + 'source-row', + { 'content-section--disabled': !searchable }, + { 'source-row source-row--error': hasError } + ); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const imageClass = classNames('source-row__icon', { 'source-row__icon--loading': isIndexing }); + + const fixLink = ( + + Fix + + ); + + const remoteTooltip = ( + <> + Remote + + + + + ); + + return ( + + + + + + + + {name} + + + + + + {status === 'need-more-config' && ( + + + + )} + + + {statusMessage} + + + + + + {isFederatedSource ? remoteTooltip : parseInt(documentCount, 10).toLocaleString('en-US')} + + {onSearchableToggle && ( + + onSearchableToggle(id, e.target.checked)} + disabled={!supportedByLicense} + compressed + label="Source Searchable Toggle" + showLabel={false} + data-test-subj="SourceSearchableToggle" + /> + + )} + + + {showFix && {fixLink}} + + {showDetails && ( + + Details + + )} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/index.ts new file mode 100644 index 0000000000000..bb5f490ed472b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/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 { SourcesTable } from './sources_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx new file mode 100644 index 0000000000000..dfac8525471b4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiTable } from '@elastic/eui'; +import { TableHeader } from '../../../../shared/table_header/table_header'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import { SourceRow } from '../source_row'; + +import { SourcesTable } from './'; + +const onToggle = jest.fn(); + +describe('SourcesTable', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(SourceRow)).toHaveLength(2); + }); + + it('renders "Searchable" header item when toggle fn present', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(TableHeader).prop('headerItems')).toContain('Searchable'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx new file mode 100644 index 0000000000000..184aadc1dad84 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.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 React from 'react'; + +import { EuiTable, EuiTableBody } from '@elastic/eui'; + +import { TableHeader } from '../../../../shared/table_header/table_header'; +import { SourceRow, ISourceRow } from '../source_row'; +import { IContentSourceDetails } from '../../../types'; + +interface ISourcesTableProps extends ISourceRow { + sources: IContentSourceDetails[]; +} + +export const SourcesTable: React.FC = ({ + sources, + showDetails, + isOrganization, + onSearchableToggle, +}) => { + const headerItems = ['Source', 'Status', 'Documents']; + if (onSearchableToggle) headerItems.push('Searchable'); + + return ( + + + + {sources.map((source) => ( + + ))} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/index.ts new file mode 100644 index 0000000000000..1598ee760c059 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_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 { TablePaginationBar } from './table_pagination_bar'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx new file mode 100644 index 0000000000000..14d4dfecd0094 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiFlexGroup, EuiTablePagination } from '@elastic/eui'; + +import { TablePaginationBar } from './'; + +const onChange = jest.fn(); + +describe('TablePaginationBar', () => { + it('renders', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiTablePagination)).toHaveLength(1); + expect(wrapper.find(EuiFlexGroup)).toHaveLength(2); + expect(wrapper.find(EuiTablePagination).prop('itemsPerPage')).toEqual(10); + expect(wrapper.find(EuiTablePagination).prop('pageCount')).toEqual(1); + }); + + it('passes max page count when showAllPages false', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiTablePagination).prop('pageCount')).toEqual(100); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.tsx new file mode 100644 index 0000000000000..04f5b9a477058 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.tsx @@ -0,0 +1,82 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiTablePagination } from '@elastic/eui'; + +interface ITablePaginationBarProps { + itemLabel?: string; + itemsPerPage?: number; + totalPages: number; + totalItems: number; + activePage?: number; + hidePerPageOptions?: boolean; + hideLabelCount?: boolean; + clearFiltersLink?: React.ReactElement; + onChangePage(nextPage: number): void; +} + +const MAX_PAGES = 100; + +export const TablePaginationBar: React.FC = ({ + itemLabel = 'Items', + itemsPerPage = 10, + totalPages, + totalItems, + activePage = 1, + hidePerPageOptions = true, + hideLabelCount = false, + onChangePage, + clearFiltersLink, +}) => { + // EUI component starts page at 0. API starts at 1. + const currentPage = activePage - 1; + const showAllPages = totalPages < MAX_PAGES; + + const pageRangeText = () => { + const rangeEnd = activePage === totalPages ? totalItems : itemsPerPage * activePage; + const rangeStart = currentPage * itemsPerPage + 1; + const rangeEl = ( + + {rangeStart}-{rangeEnd} + + ); + const totalEl = {totalItems.toLocaleString()}; + + return ( + + +
    + Showing {rangeEl} + {showAllPages && ( + <> + {' '} + of {totalEl} {itemLabel} + + )} +
    +
    + {clearFiltersLink} +
    + ); + }; + + return ( + + {!hideLabelCount && {pageRangeText()}} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/index.ts new file mode 100644 index 0000000000000..e9ee92bed68ca --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/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 { UserIcon } from './user_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx new file mode 100644 index 0000000000000..f8c1d00047825 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { users } from '../../../__mocks__/users.mock'; + +import { UserIcon } from './user_icon'; + +describe('SourcesTable', () => { + it('renders with picture', () => { + const wrapper = shallow(); + + expect(wrapper.find('.avatar')).toHaveLength(1); + expect(wrapper.find('.avatar__image')).toHaveLength(1); + }); + + it('renders without picture', () => { + const user = { + ...users[0], + pictureUrl: null, + }; + const wrapper = shallow(); + + expect(wrapper.find('.avatar')).toHaveLength(1); + expect(wrapper.find('.avatar__text')).toHaveLength(1); + }); + + it('renders fallback "alt" value when name not present', () => { + const user = { + ...users[0], + name: null, + }; + const wrapper = shallow(); + + expect(wrapper.find('img').prop('alt')).toEqual(user.email); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx new file mode 100644 index 0000000000000..28ea303d14eaf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.tsx @@ -0,0 +1,19 @@ +/* + * 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 { IUser } from '../../../types'; + +export const UserIcon: React.FC = ({ name, pictureUrl, color, initials, email }) => ( +
    + {pictureUrl ? ( + {name + ) : ( + {initials} + )} +
    +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/index.ts new file mode 100644 index 0000000000000..cc4650d70dad0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/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 { UserRow } from './user_row'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx new file mode 100644 index 0000000000000..20d2732e8c82a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiTableRow } from '@elastic/eui'; + +import { users } from '../../../__mocks__/users.mock'; + +import { UserRow } from './'; + +describe('SourcesTable', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + expect(wrapper.find('span')).toHaveLength(0); + }); + + it('renders with email visible', () => { + const wrapper = shallow(); + + expect(wrapper.find('span')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.tsx new file mode 100644 index 0000000000000..45866c10a5b21 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.tsx @@ -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 React from 'react'; + +import { EuiTableRow, EuiTableRowCell } from '@elastic/eui'; + +import { IUser } from '../../../types'; + +interface IUserRowProps { + user: IUser; + showEmail?: boolean; +} + +export const UserRow: React.FC = ({ user: { name, email }, showEmail }) => ( + + {name} + {showEmail && {email}} + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts new file mode 100644 index 0000000000000..9f313a6995ad5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -0,0 +1,19 @@ +/* + * 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 MAX_TABLE_ROW_ICONS = 3; + +export const SOURCE_STATUSES = { + INDEXING: 'indexing', + SYNCED: 'synced', + SYNCING: 'syncing', + AWAITING_USER_ACTION: 'awaiting_user_action', + ERROR: 'error', + DISCONNECTED: 'disconnected', + ALWAYS_SYNCED: 'always_synced', +}; + +export const CUSTOM_SERVICE_TYPE = 'custom'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index c0a51d5670a14..6a51b49869eaf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -8,10 +8,11 @@ import React, { useContext, useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; +import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; import { IInitialAppData } from '../../../common/types'; import { KibanaContext, IKibanaContext } from '../index'; -import { HttpLogic, IHttpLogicValues } from '../shared/http'; -import { AppLogic, IAppActions, IAppValues } from './app_logic'; +import { HttpLogic } from '../shared/http'; +import { AppLogic } from './app_logic'; import { Layout } from '../shared/layout'; import { WorkplaceSearchNav } from './components/layout/nav'; @@ -19,6 +20,7 @@ import { SETUP_GUIDE_PATH } from './routes'; import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; +import { NotFound } from '../shared/not_found'; import { Overview } from './views/overview'; export const WorkplaceSearch: React.FC = (props) => { @@ -27,9 +29,9 @@ export const WorkplaceSearch: React.FC = (props) => { }; export const WorkplaceSearchConfigured: React.FC = (props) => { - const { hasInitialized } = useValues(AppLogic) as IAppValues; - const { initializeAppData } = useActions(AppLogic) as IAppActions; - const { errorConnecting } = useValues(HttpLogic) as IHttpLogicValues; + const { hasInitialized } = useValues(AppLogic); + const { initializeAppData } = useActions(AppLogic); + const { errorConnecting } = useValues(HttpLogic); useEffect(() => { if (!hasInitialized) initializeAppData(props); @@ -53,6 +55,9 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { {/* Will replace with groups component subsequent PR */}
    + + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx new file mode 100644 index 0000000000000..d03c0abb441b9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiLink } from '@elastic/eui'; + +import { + getContentSourcePath, + SOURCES_PATH, + ORG_SOURCES_PATH, + SOURCE_DETAILS_PATH, +} from './routes'; + +const TestComponent = ({ id, isOrg }: { id: string; isOrg?: boolean }) => { + const href = getContentSourcePath(SOURCE_DETAILS_PATH, id, !!isOrg); + return test; +}; + +describe('getContentSourcePath', () => { + it('should format org route', () => { + const wrapper = shallow(); + const path = wrapper.find(EuiLink).prop('href'); + + expect(path).toEqual(`${ORG_SOURCES_PATH}/123`); + }); + + it('should format user route', () => { + const wrapper = shallow(); + const path = wrapper.find(EuiLink).prop('href'); + + expect(path).toEqual(`${SOURCES_PATH}/123`); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 993a1a378e738..e833dde4c1b72 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { generatePath } from 'react-router-dom'; + import { CURRENT_MAJOR_VERSION } from '../../../common/version'; export const SETUP_GUIDE_PATH = '/setup_guide'; @@ -107,4 +109,8 @@ export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`; export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`; export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`; -export const getSourcePath = (sourceId: string): string => `${ORG_SOURCES_PATH}/${sourceId}`; +export const getContentSourcePath = ( + path: string, + sourceId: string, + isOrganization: boolean +): string => generatePath(isOrganization ? ORG_PATH + path : path, { sourceId }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index a8348a6f69a39..3866da738cbb6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -7,3 +7,43 @@ export * from '../../../common/types/workplace_search'; export type TSpacerSize = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'; + +export interface IGroup { + id: string; + name: string; + createdAt: string; + updatedAt: string; + contentSources: IContentSource[]; + users: IUser[]; + usersCount: number; + color?: string; +} + +export interface IUser { + id: string; + name: string | null; + initials: string; + pictureUrl: string | null; + color: string; + email: string; + role?: string; + groupIds: string[]; +} + +export interface IContentSource { + id: string; + serviceType: string; + name: string; +} + +export interface IContentSourceDetails extends IContentSource { + status: string; + statusMessage: string; + documentCount: string; + isFederatedSource: boolean; + searchable: boolean; + supportedByLicense: boolean; + errorReason: number; + allowsReauth: boolean; + boost: number; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts index e5169a51ce522..9e86993a5289d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { setMockValues, mockLogicValues, mockLogicActions } from './overview_logic.mock'; +export { setMockValues, mockValues, mockActions } from './overview_logic.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts index 05715c648e5dc..9ce3021917a21 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts @@ -7,7 +7,7 @@ import { IOverviewValues } from '../overview_logic'; import { IAccount, IOrganization } from '../../../types'; -export const mockLogicValues = { +export const mockValues = { accountsCount: 0, activityFeed: [], canCreateContentSources: false, @@ -24,21 +24,21 @@ export const mockLogicValues = { dataLoading: true, } as IOverviewValues; -export const mockLogicActions = { +export const mockActions = { initializeOverview: jest.fn(() => ({})), }; jest.mock('kea', () => ({ ...(jest.requireActual('kea') as object), - useActions: jest.fn(() => ({ ...mockLogicActions })), - useValues: jest.fn(() => ({ ...mockLogicValues })), + useActions: jest.fn(() => ({ ...mockActions })), + useValues: jest.fn(() => ({ ...mockValues })), })); import { useValues } from 'kea'; export const setMockValues = (values: object) => { (useValues as jest.Mock).mockImplementationOnce(() => ({ - ...mockLogicValues, + ...mockValues, ...values, })); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index fa4decccb34b1..5598123f1c286 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -28,7 +28,7 @@ import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; import { ContentSection } from '../../components/shared/content_section'; -import { OverviewLogic, IOverviewValues } from './overview_logic'; +import { OverviewLogic } from './overview_logic'; import { OnboardingCard } from './onboarding_card'; @@ -68,7 +68,7 @@ export const OnboardingSteps: React.FC = () => { fpAccount: { isCurated }, organization: { name, defaultOrgName }, isFederatedAuth, - } = useValues(OverviewLogic) as IOverviewValues; + } = useValues(OverviewLogic); const accountsPath = !isFederatedAuth && (canCreateInvitations || isCurated) ? USERS_PATH : undefined; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx index 53549cfcdbce7..4dc762e29deba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { ContentSection } from '../../components/shared/content_section'; import { ORG_SOURCES_PATH, USERS_PATH } from '../../routes'; -import { OverviewLogic, IOverviewValues } from './overview_logic'; +import { OverviewLogic } from './overview_logic'; import { StatisticCard } from './statistic_card'; @@ -25,7 +25,7 @@ export const OrganizationStats: React.FC = () => { accountsCount, personalSourcesCount, isFederatedAuth, - } = useValues(OverviewLogic) as IOverviewValues; + } = useValues(OverviewLogic); return ( { it('calls initialize function', async () => { mount(); - expect(mockLogicActions.initializeOverview).toHaveBeenCalled(); + expect(mockActions.initializeOverview).toHaveBeenCalled(); }); it('renders onboarding state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx index 134fc9389694d..dbc007c2aa97d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx @@ -14,7 +14,7 @@ import { useActions, useValues } from 'kea'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { OverviewLogic, IOverviewActions, IOverviewValues } from './overview_logic'; +import { OverviewLogic } from './overview_logic'; import { Loading } from '../../components/shared/loading'; import { ProductButton } from '../../components/shared/product_button'; @@ -44,7 +44,7 @@ const HEADER_DESCRIPTION = i18n.translate( ); export const Overview: React.FC = () => { - const { initializeOverview } = useActions(OverviewLogic) as IOverviewActions; + const { initializeOverview } = useActions(OverviewLogic); const { dataLoading, @@ -52,7 +52,7 @@ export const Overview: React.FC = () => { hasOrgSources, isOldAccount, organization: { name: orgName, defaultOrgName }, - } = useValues(OverviewLogic) as IOverviewValues; + } = useValues(OverviewLogic); useEffect(() => { initializeOverview(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts index 61108d7cb1f2f..6989635064ca9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts @@ -9,7 +9,7 @@ import { resetContext } from 'kea'; jest.mock('../../../shared/http', () => ({ HttpLogic: { values: { http: { get: jest.fn() } } } })); import { HttpLogic } from '../../../shared/http'; -import { mockLogicValues } from './__mocks__'; +import { mockValues } from './__mocks__'; import { OverviewLogic } from './overview_logic'; describe('OverviewLogic', () => { @@ -20,7 +20,7 @@ describe('OverviewLogic', () => { }); it('has expected default values', () => { - expect(OverviewLogic.values).toEqual(mockLogicValues); + expect(OverviewLogic.values).toEqual(mockValues); }); describe('setServerData', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts index 6606e5b55cb33..2c6846b6db7db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kea } from 'kea'; +import { kea, MakeLogicType } from 'kea'; import { HttpLogic } from '../../../shared/http'; import { IAccount, IOrganization } from '../../types'; -import { IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; import { IFeedActivity } from './recent_activity'; @@ -29,7 +28,7 @@ export interface IOverviewServerData { } export interface IOverviewActions { - setServerData(serverData: IOverviewServerData): void; + setServerData(serverData: IOverviewServerData): IOverviewServerData; initializeOverview(): void; } @@ -37,12 +36,12 @@ export interface IOverviewValues extends IOverviewServerData { dataLoading: boolean; } -export const OverviewLogic = kea({ - actions: (): IOverviewActions => ({ +export const OverviewLogic = kea>({ + actions: { setServerData: (serverData) => serverData, initializeOverview: () => null, - }), - reducers: (): TKeaReducers => ({ + }, + reducers: { organization: [ {} as IOrganization, { @@ -127,11 +126,11 @@ export const OverviewLogic = kea({ setServerData: () => false, }, ], - }), - listeners: ({ actions }): Partial => ({ + }, + listeners: ({ actions }) => ({ initializeOverview: async () => { const response = await HttpLogic.values.http.get('/api/workplace_search/overview'); actions.setServerData(response); }, }), -} as IKeaParams) as IKeaLogic; +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index ada89c33be7e2..441f45a947a49 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -15,9 +15,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ContentSection } from '../../components/shared/content_section'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; -import { getSourcePath } from '../../routes'; +import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; -import { OverviewLogic, IOverviewValues } from './overview_logic'; +import { OverviewLogic } from './overview_logic'; import './recent_activity.scss'; @@ -33,7 +33,7 @@ export const RecentActivity: React.FC = () => { const { organization: { name, defaultOrgName }, activityFeed, - } = useValues(OverviewLogic) as IOverviewValues; + } = useValues(OverviewLogic); return ( = ({ const linkProps = { onClick, target: '_blank', - href: getWorkplaceSearchUrl(getSourcePath(sourceId)), + href: getWorkplaceSearchUrl(getContentSourcePath(SOURCE_DETAILS_PATH, sourceId, true)), external: true, color: status === 'error' ? 'danger' : 'primary', 'data-test-subj': 'viewSourceDetailsLink', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/getting_started.png b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/assets/getting_started.png similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/assets/getting_started.png rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/assets/getting_started.png diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx index f9b00bdf29642..d632792f2a666 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx @@ -13,7 +13,7 @@ import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import GettingStarted from '../../assets/getting_started.png'; +import GettingStarted from './assets/getting_started.png'; const GETTING_STARTED_LINK_URL = 'https://www.elastic.co/guide/en/workplace-search/current/workplace-search-getting-started.html'; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 1ce6bae8ff603..83598a0dc971d 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -27,8 +27,6 @@ import { WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; import { ExternalUrl, IExternalUrl } from './applications/shared/enterprise_search_url'; -import AppSearchLogo from './applications/app_search/assets/logo.svg'; -import WorkplaceSearchLogo from './applications/workplace_search/assets/logo.svg'; export interface ClientConfigType { host?: string; @@ -117,7 +115,7 @@ export class EnterpriseSearchPlugin implements Plugin { plugins.home.featureCatalogue.register({ id: APP_SEARCH_PLUGIN.ID, title: APP_SEARCH_PLUGIN.NAME, - icon: AppSearchLogo, + icon: 'appSearchApp', description: APP_SEARCH_PLUGIN.DESCRIPTION, path: APP_SEARCH_PLUGIN.URL, category: FeatureCatalogueCategory.DATA, @@ -127,7 +125,7 @@ export class EnterpriseSearchPlugin implements Plugin { plugins.home.featureCatalogue.register({ id: WORKPLACE_SEARCH_PLUGIN.ID, title: WORKPLACE_SEARCH_PLUGIN.NAME, - icon: WorkplaceSearchLogo, + icon: 'workplaceSearchApp', description: WORKPLACE_SEARCH_PLUGIN.DESCRIPTION, path: WORKPLACE_SEARCH_PLUGIN.URL, category: FeatureCatalogueCategory.DATA, diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts index 189f8278f1b07..6532e06f7051c 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts @@ -22,6 +22,7 @@ describe('App Search Telemetry Usage Collector', () => { 'ui_viewed.setup_guide': 10, 'ui_viewed.engines_overview': 20, 'ui_error.cannot_connect': 3, + 'ui_error.not_found': 7, 'ui_clicked.create_first_engine_button': 40, 'ui_clicked.header_launch_button': 50, 'ui_clicked.engine_table_link': 60, @@ -60,6 +61,7 @@ describe('App Search Telemetry Usage Collector', () => { }, ui_error: { cannot_connect: 3, + not_found: 7, }, ui_clicked: { create_first_engine_button: 40, @@ -86,6 +88,7 @@ describe('App Search Telemetry Usage Collector', () => { }, ui_error: { cannot_connect: 0, + not_found: 0, }, ui_clicked: { create_first_engine_button: 0, diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts index f700088cb67a0..ede776b6e0dce 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts @@ -17,6 +17,7 @@ interface ITelemetry { }; ui_error: { cannot_connect: number; + not_found: number; }; ui_clicked: { create_first_engine_button: number; @@ -47,6 +48,7 @@ export const registerTelemetryUsageCollector = ( }, ui_error: { cannot_connect: { type: 'long' }, + not_found: { type: 'long' }, }, ui_clicked: { create_first_engine_button: { type: 'long' }, @@ -77,6 +79,7 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log }, ui_error: { cannot_connect: 0, + not_found: 0, }, ui_clicked: { create_first_engine_button: 0, @@ -97,6 +100,7 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log }, ui_error: { cannot_connect: get(savedObjectAttributes, 'ui_error.cannot_connect', 0), + not_found: get(savedObjectAttributes, 'ui_error.not_found', 0), }, ui_clicked: { create_first_engine_button: get( diff --git a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts index 8960d6fa9b67b..f7d73cb232b8d 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.test.ts @@ -22,6 +22,7 @@ describe('Workplace Search Telemetry Usage Collector', () => { 'ui_viewed.setup_guide': 10, 'ui_viewed.overview': 20, 'ui_error.cannot_connect': 3, + 'ui_error.not_found': 7, 'ui_clicked.header_launch_button': 30, 'ui_clicked.org_name_change_button': 40, 'ui_clicked.onboarding_card_button': 50, @@ -61,6 +62,7 @@ describe('Workplace Search Telemetry Usage Collector', () => { }, ui_error: { cannot_connect: 3, + not_found: 7, }, ui_clicked: { header_launch_button: 30, @@ -88,6 +90,7 @@ describe('Workplace Search Telemetry Usage Collector', () => { }, ui_error: { cannot_connect: 0, + not_found: 0, }, ui_clicked: { header_launch_button: 0, diff --git a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts index 892de5cfee35e..cc08febf41f45 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts @@ -17,6 +17,7 @@ interface ITelemetry { }; ui_error: { cannot_connect: number; + not_found: number; }; ui_clicked: { header_launch_button: number; @@ -48,6 +49,7 @@ export const registerTelemetryUsageCollector = ( }, ui_error: { cannot_connect: { type: 'long' }, + not_found: { type: 'long' }, }, ui_clicked: { header_launch_button: { type: 'long' }, @@ -79,6 +81,7 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log }, ui_error: { cannot_connect: 0, + not_found: 0, }, ui_clicked: { header_launch_button: 0, @@ -100,6 +103,7 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log }, ui_error: { cannot_connect: get(savedObjectAttributes, 'ui_error.cannot_connect', 0), + not_found: get(savedObjectAttributes, 'ui_error.not_found', 0), }, ui_clicked: { header_launch_button: get(savedObjectAttributes, 'ui_clicked.header_launch_button', 0), diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 3d85c2e9eb751..00d578f5ca866 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -4,23 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ import { coreMock, savedObjectsServiceMock } from 'src/core/server/mocks'; - import { Plugin } from './plugin'; -const initContext = coreMock.createPluginInitializerContext(); -const coreSetup = coreMock.createSetup(); -const coreStart = coreMock.createStart(); -const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock(); -typeRegistry.getVisibleTypes.mockReturnValue([ - { - name: 'foo', - hidden: false, - mappings: { properties: {} }, - namespaceType: 'single' as 'single', - }, -]); -coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); describe('Features Plugin', () => { + let initContext: ReturnType; + let coreSetup: ReturnType; + let coreStart: ReturnType; + let typeRegistry: ReturnType; + + beforeEach(() => { + initContext = coreMock.createPluginInitializerContext(); + coreSetup = coreMock.createSetup(); + coreStart = coreMock.createStart(); + typeRegistry = savedObjectsServiceMock.createTypeRegistryMock(); + typeRegistry.getVisibleTypes.mockReturnValue([ + { + name: 'foo', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'single' as 'single', + }, + ]); + coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + }); + it('returns OSS + registered features', async () => { const plugin = new Plugin(initContext); const { registerFeature } = await plugin.setup(coreSetup, {}); @@ -88,4 +95,12 @@ describe('Features Plugin', () => { expect(soTypes.includes('foo')).toBe(true); expect(soTypes.includes('bar')).toBe(false); }); + + it('registers a capabilities provider', async () => { + const plugin = new Plugin(initContext); + await plugin.setup(coreSetup, {}); + + expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); + expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledWith(expect.any(Function)); + }); }); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 5783b20eae648..61b66d95ca44f 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -61,10 +61,15 @@ export class Plugin { featureRegistry: this.featureRegistry, }); + const getFeaturesUICapabilities = () => + uiCapabilitiesForFeatures(this.featureRegistry.getAll()); + + core.capabilities.registerProvider(getFeaturesUICapabilities); + return deepFreeze({ registerFeature: this.featureRegistry.register.bind(this.featureRegistry), getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), - getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(this.featureRegistry.getAll()), + getFeaturesUICapabilities, }); } diff --git a/x-pack/plugins/global_search_providers/public/providers/application.test.ts b/x-pack/plugins/global_search_providers/public/providers/application.test.ts index 7d4143f9bcfa7..385ce91d8f981 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.test.ts @@ -25,7 +25,6 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ id: 'app1', title: 'App 1', appRoute: '/app/app1', - legacy: false, status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts index 1c5a446b8e564..0c9daadf14fc6 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts @@ -4,30 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AppNavLinkStatus, AppStatus, PublicAppInfo, PublicLegacyAppInfo } from 'src/core/public'; +import { AppNavLinkStatus, AppStatus, PublicAppInfo } from 'src/core/public'; import { appToResult, getAppResults, scoreApp } from './get_app_results'; const createApp = (props: Partial = {}): PublicAppInfo => ({ id: 'app1', title: 'App 1', appRoute: '/app/app1', - legacy: false, status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, ...props, }); -const createLegacyApp = (props: Partial = {}): PublicLegacyAppInfo => ({ - id: 'app1', - title: 'App 1', - appUrl: '/app/app1', - legacy: true, - status: AppStatus.accessible, - navLinkStatus: AppNavLinkStatus.visible, - ...props, -}); - describe('getAppResults', () => { it('retrieves the matching results', () => { const apps = [ @@ -72,14 +61,6 @@ describe('scoreApp', () => { expect(scoreApp('1-2-3-4-5', createApp({ title: '123456789' }))).toBe(0); }); }); - - it('works with legacy apps', () => { - expect(scoreApp('dashboard', createLegacyApp({ title: 'dashboard' }))).toBe(100); - expect(scoreApp('dash', createLegacyApp({ title: 'dashboard' }))).toBe(90); - expect(scoreApp('board', createLegacyApp({ title: 'dashboard' }))).toBe(75); - expect(scoreApp('0123456789', createLegacyApp({ title: '012345' }))).toBe(60); - expect(scoreApp('0123456789', createLegacyApp({ title: '12345' }))).toBe(0); - }); }); describe('appToResult', () => { @@ -99,21 +80,4 @@ describe('appToResult', () => { score: 42, }); }); - - it('converts a legacy app to a result', () => { - const app = createLegacyApp({ - id: 'legacy', - title: 'Legacy', - euiIconType: 'legacyIcon', - appUrl: '/app/legacy', - }); - expect(appToResult(app, 69)).toEqual({ - id: 'legacy', - title: 'Legacy', - type: 'application', - icon: 'legacyIcon', - url: '/app/legacy', - score: 69, - }); - }); }); diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts index 1a1939230105b..4b2901254076c 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts @@ -5,12 +5,12 @@ */ import levenshtein from 'js-levenshtein'; -import { PublicAppInfo, PublicLegacyAppInfo } from 'src/core/public'; +import { PublicAppInfo } from 'src/core/public'; import { GlobalSearchProviderResult } from '../../../global_search/public'; export const getAppResults = ( term: string, - apps: Array + apps: PublicAppInfo[] ): GlobalSearchProviderResult[] => { return apps .map((app) => ({ app, score: scoreApp(term, app) })) @@ -18,7 +18,7 @@ export const getAppResults = ( .map(({ app, score }) => appToResult(app, score)); }; -export const scoreApp = (term: string, { title }: PublicAppInfo | PublicLegacyAppInfo): number => { +export const scoreApp = (term: string, { title }: PublicAppInfo): number => { term = term.toLowerCase(); title = title.toLowerCase(); @@ -43,16 +43,13 @@ export const scoreApp = (term: string, { title }: PublicAppInfo | PublicLegacyAp return 0; }; -export const appToResult = ( - app: PublicAppInfo | PublicLegacyAppInfo, - score: number -): GlobalSearchProviderResult => { +export const appToResult = (app: PublicAppInfo, score: number): GlobalSearchProviderResult => { return { id: app.id, title: app.title, type: 'application', icon: app.euiIconType, - url: app.legacy ? app.appUrl : app.appRoute, + url: app.appRoute, score, }; }; diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx index 100122af943e1..1b783df1b7d02 100644 --- a/x-pack/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx @@ -21,7 +21,6 @@ import { ReactWrapper } from 'enzyme'; import { createMockGraphStore } from '../state_management/mocks'; import { Provider } from 'react-redux'; -jest.mock('ui/new_platform'); jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); const waitForIndexPatternFetch = () => new Promise((r) => setTimeout(r)); diff --git a/x-pack/plugins/graph/public/state_management/mocks.ts b/x-pack/plugins/graph/public/state_management/mocks.ts index d32bc9a175a47..f28f51544a6ed 100644 --- a/x-pack/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/plugins/graph/public/state_management/mocks.ts @@ -17,8 +17,6 @@ import { GraphStoreDependencies, createRootReducer, GraphStore, GraphState } fro import { Workspace, GraphWorkspaceSavedObject, IndexPatternSavedObject } from '../types'; import { IndexPattern } from '../../../../../src/plugins/data/public'; -jest.mock('ui/new_platform'); - export interface MockedGraphEnvironment { store: GraphStore; mockedDeps: jest.Mocked; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap deleted file mode 100644 index 38dd49a286b58..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap +++ /dev/null @@ -1,895 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extend index management ilm banner extension should return extension when any index has lifecycle error 1`] = ` -Object { - "filter": Query { - "ast": _AST { - "_clauses": Array [ - Object { - "field": "ilm.step", - "match": "must", - "operator": "eq", - "type": "field", - "value": "ERROR", - }, - ], - "_indexedClauses": Object { - "field": Object { - "ilm.step": Array [ - Object { - "field": "ilm.step", - "match": "must", - "operator": "eq", - "type": "field", - "value": "ERROR", - }, - ], - }, - "group": Array [], - "is": Object {}, - "term": Array [], - }, - }, - "syntax": Object { - "parse": [Function], - "print": [Function], - "printClause": [Function], - }, - "text": "ilm.step:ERROR", - }, - "filterLabel": "Show errors", - "title": "1 - index has - lifecycle errors", - "type": "warning", -} -`; - -exports[`extend index management ilm filter extension should return extension when any index has lifecycle policy 1`] = ` -Array [ - Object { - "field": "ilm.managed", - "multiSelect": false, - "name": "Lifecycle status", - "options": Array [ - Object { - "value": true, - "view": "Managed", - }, - Object { - "value": false, - "view": "Unmanaged", - }, - ], - "type": "field_value_selection", - }, - Object { - "field": "ilm.phase", - "multiSelect": "or", - "name": "Lifecycle phase", - "options": Array [ - Object { - "value": "hot", - "view": "Hot", - }, - Object { - "value": "warm", - "view": "Warm", - }, - Object { - "value": "cold", - "view": "Cold", - }, - Object { - "value": "delete", - "view": "Delete", - }, - ], - "type": "field_value_selection", - }, -] -`; - -exports[`extend index management ilm summary extension should return extension when index has lifecycle error 1`] = ` - - -

    - - Index lifecycle management - -

    -
    - -
    - - - } - > -
    -
    -
    - - -
    - -
    - -
    - -
    - - Lifecycle policy - -
    -
    - -
    - - - testy - - -
    -
    - -
    - - Current action - -
    -
    - -
    - rollover -
    -
    - -
    - - Failed step - -
    -
    - -
    - check-rollover-ready -
    -
    -
    -
    -
    -
    - -
    - -
    - -
    - - Current phase - -
    -
    - -
    - hot -
    -
    - -
    - - Current action time - -
    -
    - -
    - 2018-12-07 13:02:55 -
    -
    - -
    - - - Phase definition - - -
    -
    - -
    - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="phaseExecutionPopover" - isOpen={false} - key="phaseExecutionPopover" - ownFocus={false} - panelPaddingSize="m" - withTitle={true} - > - -
    -
    - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -`; - -exports[`extend index management ilm summary extension should return extension when index has lifecycle policy 1`] = ` - - -

    - - Index lifecycle management - -

    -
    - -
    - - - - - -`; - -exports[`extend index management remove lifecycle action extension should return extension when all indices have lifecycle policy 1`] = ` -Object { - "buttonLabel": "Remove lifecycle policy", - "icon": "stopFilled", - "indexNames": Array [ - Array [ - "testy3", - "testy3", - ], - ], - "renderConfirmModal": [Function], -} -`; - -exports[`extend index management retry lifecycle action extension should return extension when all indices have lifecycle errors 1`] = ` -Object { - "buttonLabel": "Retry lifecycle step", - "icon": "play", - "indexNames": Array [ - Array [ - "testy3", - "testy3", - ], - ], - "requestMethod": [Function], - "successMessage": "Called retry lifecycle step for: \\"testy3\\", \\"testy3\\"", -} -`; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap new file mode 100644 index 0000000000000..59d1723c39488 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap @@ -0,0 +1,977 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`extend index management ilm banner extension should return extension when any index has lifecycle error 1`] = ` +Object { + "filter": Query { + "ast": _AST { + "_clauses": Array [ + Object { + "field": "ilm.step", + "match": "must", + "operator": "eq", + "type": "field", + "value": "ERROR", + }, + ], + "_indexedClauses": Object { + "field": Object { + "ilm.step": Array [ + Object { + "field": "ilm.step", + "match": "must", + "operator": "eq", + "type": "field", + "value": "ERROR", + }, + ], + }, + "group": Array [], + "is": Object {}, + "term": Array [], + }, + }, + "syntax": Object { + "parse": [Function], + "print": [Function], + "printClause": [Function], + }, + "text": "ilm.step:ERROR", + }, + "filterLabel": "Show errors", + "title": "1 + index has + lifecycle errors", + "type": "warning", +} +`; + +exports[`extend index management ilm filter extension should return extension when any index has lifecycle policy 1`] = ` +Array [ + Object { + "field": "ilm.managed", + "multiSelect": false, + "name": "Lifecycle status", + "options": Array [ + Object { + "value": true, + "view": "Managed", + }, + Object { + "value": false, + "view": "Unmanaged", + }, + ], + "type": "field_value_selection", + }, + Object { + "field": "ilm.phase", + "multiSelect": "or", + "name": "Lifecycle phase", + "options": Array [ + Object { + "value": "hot", + "view": "Hot", + }, + Object { + "value": "warm", + "view": "Warm", + }, + Object { + "value": "frozen", + "view": "Frozen", + }, + Object { + "value": "cold", + "view": "Cold", + }, + Object { + "value": "delete", + "view": "Delete", + }, + ], + "type": "field_value_selection", + }, +] +`; + +exports[`extend index management ilm summary extension should return extension when index has lifecycle error 1`] = ` + + +

    + + Index lifecycle management + +

    +
    + +
    + + + } + > +
    +
    +
    + + + + + +`; + +exports[`extend index management ilm summary extension should return extension when index has lifecycle policy 1`] = ` + + +

    + + Index lifecycle management + +

    +
    + +
    + + +
    + +
    + +
    + +
    + + Lifecycle policy + +
    +
    + +
    + + + testy + + +
    + + +
    + + Current action + +
    +
    + +
    + complete +
    +
    + +
    + + Failed step + +
    +
    + +
    + - +
    +
    +
    + +
    + + +
    + +
    + +
    + + Current phase + +
    +
    + +
    + new +
    +
    + +
    + + Current action time + +
    +
    + +
    + 2018-12-07 13:02:55 +
    +
    +
    +
    +
    +
    +
    + + +`; + +exports[`extend index management remove lifecycle action extension should return extension when all indices have lifecycle policy 1`] = ` +Object { + "buttonLabel": "Remove lifecycle policy", + "icon": "stopFilled", + "indexNames": Array [ + Array [ + "testy3", + "testy3", + ], + ], + "renderConfirmModal": [Function], +} +`; + +exports[`extend index management retry lifecycle action extension should return extension when all indices have lifecycle errors 1`] = ` +Object { + "buttonLabel": "Retry lifecycle step", + "icon": "play", + "indexNames": Array [ + Array [ + "testy3", + "testy3", + ], + ], + "requestMethod": [Function], + "successMessage": "Called retry lifecycle step for: \\"testy3\\", \\"testy3\\"", +} +`; 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.js.snap deleted file mode 100644 index ad3e0956fcf25..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap +++ /dev/null @@ -1,240 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`policy table should change pages when a pagination link is clicked on 1`] = ` -Array [ - "testy0", - "testy1", - "testy10", - "testy100", - "testy101", - "testy102", - "testy103", - "testy104", - "testy11", - "testy12", -] -`; - -exports[`policy table should change pages when a pagination link is clicked on 2`] = ` -Array [ - "testy13", - "testy14", - "testy15", - "testy16", - "testy17", - "testy18", - "testy19", - "testy2", - "testy20", - "testy21", -] -`; - -exports[`policy table should filter based on content of search input 1`] = ` -Array [ - "testy0", - "testy1", - "testy10", - "testy100", - "testy101", - "testy102", - "testy103", - "testy104", - "testy11", - "testy12", -] -`; - -exports[`policy table should show empty state when there are not any policies 1`] = ` -
    -
    -
    -
    -
    -
    -
    - -

    - Create your first index lifecycle policy -

    -
    -
    -

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

    -
    - -
    -
    - -
    -
    -
    -
    -
    -`; - -exports[`policy table should sort when linked indices header is clicked 1`] = ` -Array [ - "testy1", - "testy3", - "testy5", - "testy7", - "testy9", - "testy11", - "testy13", - "testy15", - "testy17", - "testy19", -] -`; - -exports[`policy table should sort when linked indices header is clicked 2`] = ` -Array [ - "testy104", - "testy102", - "testy100", - "testy98", - "testy96", - "testy94", - "testy92", - "testy90", - "testy88", - "testy86", -] -`; - -exports[`policy table should sort when modified date header is clicked 1`] = ` -Array [ - "testy104", - "testy103", - "testy102", - "testy101", - "testy100", - "testy99", - "testy98", - "testy97", - "testy96", - "testy95", -] -`; - -exports[`policy table should sort when modified date header is clicked 2`] = ` -Array [ - "testy0", - "testy1", - "testy2", - "testy3", - "testy4", - "testy5", - "testy6", - "testy7", - "testy8", - "testy9", -] -`; - -exports[`policy table should sort when name header is clicked 1`] = ` -Array [ - "testy99", - "testy98", - "testy97", - "testy96", - "testy95", - "testy94", - "testy93", - "testy92", - "testy91", - "testy90", -] -`; - -exports[`policy table should sort when name header is clicked 2`] = ` -Array [ - "testy0", - "testy1", - "testy10", - "testy100", - "testy101", - "testy102", - "testy103", - "testy104", - "testy11", - "testy12", -] -`; - -exports[`policy table should sort when version header is clicked 1`] = ` -Array [ - "testy0", - "testy1", - "testy2", - "testy3", - "testy4", - "testy5", - "testy6", - "testy7", - "testy8", - "testy9", -] -`; - -exports[`policy table should sort when version header is clicked 2`] = ` -Array [ - "testy104", - "testy103", - "testy102", - "testy101", - "testy100", - "testy99", - "testy98", - "testy97", - "testy96", - "testy95", -] -`; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap new file mode 100644 index 0000000000000..cbb9f82888701 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap @@ -0,0 +1,232 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`policy table should change pages when a pagination link is clicked on 1`] = ` +Array [ + "testy0", + "testy1", + "testy10", + "testy100", + "testy101", + "testy102", + "testy103", + "testy104", + "testy11", + "testy12", +] +`; + +exports[`policy table should change pages when a pagination link is clicked on 2`] = ` +Array [ + "testy13", + "testy14", + "testy15", + "testy16", + "testy17", + "testy18", + "testy19", + "testy2", + "testy20", + "testy21", +] +`; + +exports[`policy table should filter based on content of search input 1`] = ` +Array [ + "testy0", + "testy1", + "testy10", + "testy100", + "testy101", + "testy102", + "testy103", + "testy104", + "testy11", + "testy12", +] +`; + +exports[`policy table should show empty state when there are not any policies 1`] = ` +
    +
    +
    +
    +
    + +

    + Create your first index lifecycle policy +

    +
    +
    +

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

    +
    + +
    +
    + +
    +
    +
    +`; + +exports[`policy table should sort when linked indices header is clicked 1`] = ` +Array [ + "testy1", + "testy3", + "testy5", + "testy7", + "testy9", + "testy11", + "testy13", + "testy15", + "testy17", + "testy19", +] +`; + +exports[`policy table should sort when linked indices header is clicked 2`] = ` +Array [ + "testy104", + "testy102", + "testy100", + "testy98", + "testy96", + "testy94", + "testy92", + "testy90", + "testy88", + "testy86", +] +`; + +exports[`policy table should sort when modified date header is clicked 1`] = ` +Array [ + "testy104", + "testy103", + "testy102", + "testy101", + "testy100", + "testy99", + "testy98", + "testy97", + "testy96", + "testy95", +] +`; + +exports[`policy table should sort when modified date header is clicked 2`] = ` +Array [ + "testy0", + "testy1", + "testy2", + "testy3", + "testy4", + "testy5", + "testy6", + "testy7", + "testy8", + "testy9", +] +`; + +exports[`policy table should sort when name header is clicked 1`] = ` +Array [ + "testy99", + "testy98", + "testy97", + "testy96", + "testy95", + "testy94", + "testy93", + "testy92", + "testy91", + "testy90", +] +`; + +exports[`policy table should sort when name header is clicked 2`] = ` +Array [ + "testy0", + "testy1", + "testy10", + "testy100", + "testy101", + "testy102", + "testy103", + "testy104", + "testy11", + "testy12", +] +`; + +exports[`policy table should sort when version header is clicked 1`] = ` +Array [ + "testy0", + "testy1", + "testy2", + "testy3", + "testy4", + "testy5", + "testy6", + "testy7", + "testy8", + "testy9", +] +`; + +exports[`policy table should sort when version header is clicked 2`] = ` +Array [ + "testy104", + "testy103", + "testy102", + "testy101", + "testy100", + "testy99", + "testy98", + "testy97", + "testy96", + "testy95", +] +`; 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.js deleted file mode 100644 index 60e3e9443bec9..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js +++ /dev/null @@ -1,174 +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 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 { 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 { init as initHttp } from '../../public/application/services/http'; -import { init as initUiMetric } from '../../public/application/services/ui_metric'; - -initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path); -initUiMetric({ reportUiStats: () => {} }); - -let server = null; - -let store = null; -const policies = []; -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, - name: `testy${i}`, - }); -} -jest.mock(''); -let component = null; - -const snapshot = (rendered) => { - expect(rendered).toMatchSnapshot(); -}; -const mountedSnapshot = (rendered) => { - expect(takeMountedSnapshot(rendered)).toMatchSnapshot(); -}; -const names = (rendered) => { - return findTestSubject(rendered, 'policyTablePolicyNameLink'); -}; -const namesText = (rendered) => { - return names(rendered).map((button) => button.text()); -}; - -const testSort = (headerName) => { - const rendered = mountWithIntl(component); - const nameHeader = findTestSubject(rendered, `policyTableHeaderCell-${headerName}`).find( - 'button' - ); - nameHeader.simulate('click'); - rendered.update(); - snapshot(namesText(rendered)); - nameHeader.simulate('click'); - rendered.update(); - snapshot(namesText(rendered)); -}; -const openContextMenu = (buttonIndex) => { - const rendered = mountWithIntl(component); - const actionsButton = findTestSubject(rendered, 'policyActionsContextMenuButton'); - actionsButton.at(buttonIndex).simulate('click'); - rendered.update(); - return rendered; -}; - -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(); - 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', () => { - const rendered = mountWithIntl(component); - snapshot(namesText(rendered)); - const pagingButtons = rendered.find('.euiPaginationButton'); - pagingButtons.at(2).simulate('click'); - rendered.update(); - snapshot(namesText(rendered)); - }); - test('should show more when per page value is increased', () => { - const rendered = mountWithIntl(component); - const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button'); - perPageButton.simulate('click'); - rendered.update(); - const fiftyButton = rendered.find('.euiContextMenuItem').at(1); - fiftyButton.simulate('click'); - rendered.update(); - expect(namesText(rendered).length).toBe(50); - }); - test('should filter based on content of search input', () => { - const rendered = mountWithIntl(component); - const searchInput = rendered.find('.euiFieldSearch').first(); - searchInput.instance().value = 'testy0'; - searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); - rendered.update(); - snapshot(namesText(rendered)); - }); - test('should sort when name header is clicked', () => { - testSort('name'); - }); - test('should sort when version header is clicked', () => { - testSort('version'); - }); - test('should sort when modified date header is clicked', () => { - testSort('modified_date'); - }); - test('should sort when linked indices header is clicked', () => { - testSort('linkedIndices'); - }); - test('should have proper actions in context menu when there are linked indices', () => { - const rendered = openContextMenu(0); - const buttons = rendered.find('button.euiContextMenuItem'); - expect(buttons.length).toBe(3); - 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(); - }); - test('should have proper actions in context menu when there are not linked indices', () => { - const rendered = openContextMenu(1); - const buttons = rendered.find('button.euiContextMenuItem'); - 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(); - }); - test('confirmation modal should show when delete button is pressed', () => { - const rendered = openContextMenu(1); - const deleteButton = rendered.find('button.euiContextMenuItem').at(1); - deleteButton.simulate('click'); - rendered.update(); - expect(rendered.find('.euiModal--confirmation').exists()).toBeTruthy(); - }); - test('confirmation modal should show when add policy to index template button is pressed', () => { - const rendered = openContextMenu(1); - const deleteButton = rendered.find('button.euiContextMenuItem').at(0); - deleteButton.simulate('click'); - rendered.update(); - expect(rendered.find('.euiModal--confirmation').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx new file mode 100644 index 0000000000000..d95b4503c266b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment-timezone'; +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 { + 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( + new HttpService().setup({ + injectedMetadata: injectedMetadataServiceMock.createSetupContract(), + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + }) +); +initUiMetric(usageCollectionPluginMock.createSetupContract()); + +const policies: PolicyFromES[] = []; +for (let i = 0; i < 105; i++) { + policies.push({ + version: i, + 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: ReactElement; + +const snapshot = (rendered: string[]) => { + expect(rendered).toMatchSnapshot(); +}; +const mountedSnapshot = (rendered: ReactWrapper) => { + expect(takeMountedSnapshot(rendered)).toMatchSnapshot(); +}; +const names = (rendered: ReactWrapper) => { + return findTestSubject(rendered, 'policyTablePolicyNameLink'); +}; +const namesText = (rendered: ReactWrapper): string[] => { + return (names(rendered) as ReactWrapper).map((button) => button.text()); +}; + +const testSort = (headerName: string) => { + const rendered = mountWithIntl(component); + const nameHeader = findTestSubject(rendered, `policyTableHeaderCell-${headerName}`).find( + 'button' + ); + nameHeader.simulate('click'); + rendered.update(); + snapshot(namesText(rendered)); + nameHeader.simulate('click'); + rendered.update(); + snapshot(namesText(rendered)); +}; +const openContextMenu = (buttonIndex: number) => { + const rendered = mountWithIntl(component); + const actionsButton = findTestSubject(rendered, 'policyActionsContextMenuButton'); + actionsButton.at(buttonIndex).simulate('click'); + rendered.update(); + return rendered; +}; + +describe('policy table', () => { + beforeEach(() => { + component = ( + + ); + }); + + test('should show empty state when there are not any policies', () => { + component = ( + + ); + const rendered = mountWithIntl(component); + mountedSnapshot(rendered); + }); + test('should change pages when a pagination link is clicked on', () => { + const rendered = mountWithIntl(component); + snapshot(namesText(rendered)); + const pagingButtons = rendered.find('.euiPaginationButton'); + pagingButtons.at(2).simulate('click'); + rendered.update(); + snapshot(namesText(rendered)); + }); + test('should show more when per page value is increased', () => { + const rendered = mountWithIntl(component); + const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button'); + perPageButton.simulate('click'); + rendered.update(); + const fiftyButton = rendered.find('.euiContextMenuItem').at(1); + fiftyButton.simulate('click'); + rendered.update(); + expect(namesText(rendered).length).toBe(50); + }); + test('should filter based on content of search input', () => { + const rendered = mountWithIntl(component); + const searchInput = rendered.find('.euiFieldSearch').first(); + ((searchInput.instance() as unknown) as HTMLInputElement).value = 'testy0'; + searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); + rendered.update(); + snapshot(namesText(rendered)); + }); + test('should sort when name header is clicked', () => { + testSort('name'); + }); + test('should sort when version header is clicked', () => { + testSort('version'); + }); + test('should sort when modified date header is clicked', () => { + testSort('modified_date'); + }); + test('should sort when linked indices header is clicked', () => { + testSort('linkedIndices'); + }); + test('should have proper actions in context menu when there are linked indices', () => { + const rendered = openContextMenu(0); + const buttons = rendered.find('button.euiContextMenuItem'); + expect(buttons.length).toBe(3); + 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() as HTMLButtonElement).disabled).toBeTruthy(); + }); + test('should have proper actions in context menu when there are not linked indices', () => { + const rendered = openContextMenu(1); + const buttons = rendered.find('button.euiContextMenuItem'); + 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() as HTMLButtonElement).disabled).toBeFalsy(); + }); + test('confirmation modal should show when delete button is pressed', () => { + const rendered = openContextMenu(1); + const deleteButton = rendered.find('button.euiContextMenuItem').at(1); + deleteButton.simulate('click'); + rendered.update(); + expect(rendered.find('.euiModal--confirmation').exists()).toBeTruthy(); + }); + test('confirmation modal should show when add policy to index template button is pressed', () => { + const rendered = openContextMenu(1); + const deleteButton = rendered.find('button.euiContextMenuItem').at(0); + deleteButton.simulate('click'); + rendered.update(); + expect(rendered.find('.euiModal--confirmation').exists()).toBeTruthy(); + }); +}); 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.js deleted file mode 100644 index 4fa1838115840..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js +++ /dev/null @@ -1,271 +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 moment from 'moment-timezone'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; - -import { mountWithIntl } from '../../../test_utils/enzyme_helpers'; -import { - retryLifecycleActionExtension, - removeLifecyclePolicyActionExtension, - addLifecyclePolicyActionExtension, - ilmBannerExtension, - ilmFilterExtension, - ilmSummaryExtension, -} from '../public/extend_index_management'; -import { init as initHttp } from '../public/application/services/http'; -import { init as initUiMetric } from '../public/application/services/ui_metric'; - -// 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: () => {} }); - -jest.mock('../../../plugins/index_management/public', async () => { - const { indexManagementMock } = await import('../../../plugins/index_management/public/mocks.ts'); - return indexManagementMock.createSetup(); -}); - -const indexWithoutLifecyclePolicy = { - health: 'yellow', - status: 'open', - name: 'noPolicy', - uuid: 'BJ-nrZYuSrG-jaofr6SeLg', - primary: '1', - replica: '1', - documents: '1', - documents_deleted: '0', - size: '3.4kb', - primary_size: '3.4kb', - aliases: 'none', - ilm: { - index: 'testy1', - managed: false, - }, -}; - -const indexWithLifecyclePolicy = { - health: 'yellow', - status: 'open', - name: 'testy3', - uuid: 'XL11TLa3Tvq298_dMUzLHQ', - primary: '1', - replica: '1', - documents: '2', - documents_deleted: '0', - size: '6.5kb', - primary_size: '6.5kb', - aliases: 'none', - ilm: { - index: 'testy3', - managed: true, - policy: 'testy', - lifecycle_date_millis: 1544020872361, - phase: 'new', - phase_time_millis: 1544187775867, - action: 'complete', - action_time_millis: 1544187775867, - step: 'complete', - step_time_millis: 1544187775867, - }, -}; - -const indexWithLifecycleError = { - health: 'yellow', - status: 'open', - name: 'testy3', - uuid: 'XL11TLa3Tvq298_dMUzLHQ', - primary: '1', - replica: '1', - documents: '2', - documents_deleted: '0', - size: '6.5kb', - primary_size: '6.5kb', - aliases: 'none', - ilm: { - index: 'testy3', - managed: true, - policy: 'testy', - lifecycle_date_millis: 1544020872361, - phase: 'hot', - phase_time_millis: 1544187775891, - action: 'rollover', - action_time_millis: 1544187775891, - step: 'ERROR', - step_time_millis: 1544187776208, - failed_step: 'check-rollover-ready', - step_info: { - type: 'illegal_argument_exception', - reason: 'setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined', - stack_trace: 'fakestacktrace', - }, - phase_execution: { - policy: 'testy', - phase_definition: { min_age: '0s', actions: { rollover: { max_size: '1gb' } } }, - version: 1, - modified_date_in_millis: 1544031699844, - }, - }, -}; - -moment.tz.setDefault('utc'); - -const getUrlForApp = (appId, options) => { - return appId + '/' + (options ? options.path : ''); -}; - -describe('extend index management', () => { - describe('retry lifecycle action extension', () => { - test('should return null when no indices have index lifecycle policy', () => { - const extension = retryLifecycleActionExtension({ indices: [indexWithoutLifecyclePolicy] }); - expect(extension).toBeNull(); - }); - - test('should return null when no index has lifecycle errors', () => { - const extension = retryLifecycleActionExtension({ - indices: [indexWithLifecyclePolicy, indexWithLifecyclePolicy], - }); - expect(extension).toBeNull(); - }); - - test('should return null when not all indices have lifecycle errors', () => { - const extension = retryLifecycleActionExtension({ - indices: [indexWithLifecyclePolicy, indexWithLifecycleError], - }); - expect(extension).toBeNull(); - }); - - test('should return extension when all indices have lifecycle errors', () => { - const extension = retryLifecycleActionExtension({ - indices: [indexWithLifecycleError, indexWithLifecycleError], - }); - expect(extension).toBeDefined(); - expect(extension).toMatchSnapshot(); - }); - }); - - describe('remove lifecycle action extension', () => { - test('should return null when no indices have index lifecycle policy', () => { - const extension = removeLifecyclePolicyActionExtension({ - indices: [indexWithoutLifecyclePolicy], - }); - expect(extension).toBeNull(); - }); - - test('should return null when some indices have index lifecycle policy', () => { - const extension = removeLifecyclePolicyActionExtension({ - indices: [indexWithoutLifecyclePolicy, indexWithLifecyclePolicy], - }); - expect(extension).toBeNull(); - }); - - test('should return extension when all indices have lifecycle policy', () => { - const extension = removeLifecyclePolicyActionExtension({ - indices: [indexWithLifecycleError, indexWithLifecycleError], - }); - expect(extension).toBeDefined(); - expect(extension).toMatchSnapshot(); - }); - }); - - describe('add lifecycle policy action extension', () => { - test('should return null when index has index lifecycle policy', () => { - const extension = addLifecyclePolicyActionExtension( - { indices: [indexWithLifecyclePolicy] }, - getUrlForApp - ); - expect(extension).toBeNull(); - }); - - test('should return null when more than one index is passed', () => { - const extension = addLifecyclePolicyActionExtension({ - indices: [indexWithoutLifecyclePolicy, indexWithoutLifecyclePolicy], - getUrlForApp, - }); - expect(extension).toBeNull(); - }); - - test('should return extension when one index is passed and it does not have lifecycle policy', () => { - const extension = addLifecyclePolicyActionExtension({ - indices: [indexWithoutLifecyclePolicy], - getUrlForApp, - }); - expect(extension.renderConfirmModal).toBeDefined; - const component = extension.renderConfirmModal(jest.fn()); - const rendered = mountWithIntl(component); - expect(rendered.exists('.euiModal--confirmation')); - }); - }); - - describe('ilm banner extension', () => { - test('should return null when no index has index lifecycle policy', () => { - const extension = ilmBannerExtension([ - indexWithoutLifecyclePolicy, - indexWithoutLifecyclePolicy, - ]); - expect(extension).toBeNull(); - }); - - test('should return null no index has lifecycle error', () => { - const extension = ilmBannerExtension([indexWithoutLifecyclePolicy, indexWithLifecyclePolicy]); - expect(extension).toBeNull(); - }); - - test('should return extension when any index has lifecycle error', () => { - const extension = ilmBannerExtension([ - indexWithoutLifecyclePolicy, - indexWithLifecyclePolicy, - indexWithLifecycleError, - ]); - expect(extension).toBeDefined(); - expect(extension).toMatchSnapshot(); - }); - }); - - describe('ilm summary extension', () => { - test('should render null when index has no index lifecycle policy', () => { - const extension = ilmSummaryExtension(indexWithoutLifecyclePolicy, getUrlForApp); - const rendered = mountWithIntl(extension); - expect(rendered.isEmptyRender()).toBeTruthy(); - }); - - test('should return extension when index has lifecycle policy', () => { - const extension = ilmSummaryExtension(indexWithLifecyclePolicy, getUrlForApp); - expect(extension).toBeDefined(); - const rendered = mountWithIntl(extension); - expect(rendered).toMatchSnapshot(); - }); - - test('should return extension when index has lifecycle error', () => { - const extension = ilmSummaryExtension(indexWithLifecycleError, getUrlForApp); - expect(extension).toBeDefined(); - const rendered = mountWithIntl(extension); - expect(rendered).toMatchSnapshot(); - }); - }); - - describe('ilm filter extension', () => { - test('should return empty array when no indices have index lifecycle policy', () => { - const extension = ilmFilterExtension([ - indexWithoutLifecyclePolicy, - indexWithoutLifecyclePolicy, - ]); - expect(extension.length).toBe(0); - }); - - test('should return extension when any index has lifecycle policy', () => { - const extension = ilmFilterExtension([ - indexWithLifecyclePolicy, - indexWithoutLifecyclePolicy, - indexWithoutLifecyclePolicy, - ]); - expect(extension).toBeDefined(); - expect(extension).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx new file mode 100644 index 0000000000000..17573cb81c408 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -0,0 +1,286 @@ +/* + * 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-timezone'; +import axios from 'axios'; +import axiosXhrAdapter from 'axios/lib/adapters/xhr'; + +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks'; +import { + retryLifecycleActionExtension, + removeLifecyclePolicyActionExtension, + addLifecyclePolicyActionExtension, + ilmBannerExtension, + ilmFilterExtension, + ilmSummaryExtension, +} 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". +// 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'); + return indexManagementMock.createSetup(); +}); + +const indexWithoutLifecyclePolicy: Index = { + health: 'yellow', + status: 'open', + name: 'noPolicy', + uuid: 'BJ-nrZYuSrG-jaofr6SeLg', + primary: '1', + replica: '1', + documents: '1', + documents_deleted: '0', + size: '3.4kb', + primary_size: '3.4kb', + aliases: 'none', + isFrozen: false, + ilm: { + index: 'testy1', + managed: false, + }, +}; + +const indexWithLifecyclePolicy: Index = { + health: 'yellow', + status: 'open', + name: 'testy3', + uuid: 'XL11TLa3Tvq298_dMUzLHQ', + primary: '1', + replica: '1', + documents: '2', + documents_deleted: '0', + size: '6.5kb', + primary_size: '6.5kb', + aliases: 'none', + isFrozen: false, + ilm: { + index: 'testy3', + managed: true, + policy: 'testy', + lifecycle_date_millis: 1544020872361, + phase: 'new', + phase_time_millis: 1544187775867, + action: 'complete', + action_time_millis: 1544187775867, + step: 'complete', + step_time_millis: 1544187775867, + }, +}; + +const indexWithLifecycleError = { + health: 'yellow', + status: 'open', + name: 'testy3', + uuid: 'XL11TLa3Tvq298_dMUzLHQ', + primary: '1', + replica: '1', + documents: '2', + documents_deleted: '0', + size: '6.5kb', + primary_size: '6.5kb', + aliases: 'none', + isFrozen: false, + ilm: { + index: 'testy3', + managed: true, + policy: 'testy', + lifecycle_date_millis: 1544020872361, + phase: 'hot', + phase_time_millis: 1544187775891, + action: 'rollover', + action_time_millis: 1544187775891, + step: 'ERROR', + step_time_millis: 1544187776208, + failed_step: 'check-rollover-ready', + step_info: { + type: 'illegal_argument_exception', + reason: 'setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined', + stack_trace: 'fakestacktrace', + }, + phase_execution: { + policy: 'testy', + phase_definition: { min_age: '0s', actions: { rollover: { max_size: '1gb' } } }, + version: 1, + modified_date_in_millis: 1544031699844, + }, + }, +}; + +moment.tz.setDefault('utc'); + +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', () => { + const extension = retryLifecycleActionExtension({ indices: [indexWithoutLifecyclePolicy] }); + expect(extension).toBeNull(); + }); + + test('should return null when no index has lifecycle errors', () => { + const extension = retryLifecycleActionExtension({ + indices: [indexWithLifecyclePolicy, indexWithLifecyclePolicy], + }); + expect(extension).toBeNull(); + }); + + test('should return null when not all indices have lifecycle errors', () => { + const extension = retryLifecycleActionExtension({ + indices: [indexWithLifecyclePolicy, indexWithLifecycleError], + }); + expect(extension).toBeNull(); + }); + + test('should return extension when all indices have lifecycle errors', () => { + const extension = retryLifecycleActionExtension({ + indices: [indexWithLifecycleError, indexWithLifecycleError], + }); + expect(extension).toBeDefined(); + expect(extension).toMatchSnapshot(); + }); + }); + + describe('remove lifecycle action extension', () => { + test('should return null when no indices have index lifecycle policy', () => { + const extension = removeLifecyclePolicyActionExtension({ + indices: [indexWithoutLifecyclePolicy], + reloadIndices, + }); + expect(extension).toBeNull(); + }); + + test('should return null when some indices have index lifecycle policy', () => { + const extension = removeLifecyclePolicyActionExtension({ + indices: [indexWithoutLifecyclePolicy, indexWithLifecyclePolicy], + reloadIndices, + }); + expect(extension).toBeNull(); + }); + + test('should return extension when all indices have lifecycle policy', () => { + const extension = removeLifecyclePolicyActionExtension({ + indices: [indexWithLifecycleError, indexWithLifecycleError], + reloadIndices, + }); + expect(extension).toBeDefined(); + expect(extension).toMatchSnapshot(); + }); + }); + + describe('add lifecycle policy action extension', () => { + test('should return null when index has index lifecycle policy', () => { + 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(); + }); + + 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()); + const rendered = mountWithIntl(component); + expect(rendered.exists('.euiModal--confirmation')); + }); + }); + + describe('ilm banner extension', () => { + test('should return null when no index has index lifecycle policy', () => { + const extension = ilmBannerExtension([ + indexWithoutLifecyclePolicy, + indexWithoutLifecyclePolicy, + ]); + expect(extension).toBeNull(); + }); + + test('should return null no index has lifecycle error', () => { + const extension = ilmBannerExtension([indexWithoutLifecyclePolicy, indexWithLifecyclePolicy]); + expect(extension).toBeNull(); + }); + + test('should return extension when any index has lifecycle error', () => { + const extension = ilmBannerExtension([ + indexWithoutLifecyclePolicy, + indexWithLifecyclePolicy, + indexWithLifecycleError, + ]); + expect(extension).toBeDefined(); + expect(extension).toMatchSnapshot(); + }); + }); + + describe('ilm summary extension', () => { + test('should render null when index has no index lifecycle policy', () => { + const extension = ilmSummaryExtension(indexWithoutLifecyclePolicy, getUrlForApp); + const rendered = mountWithIntl(extension); + expect(rendered.isEmptyRender()).toBeTruthy(); + }); + + test('should return extension when index has lifecycle policy', () => { + const extension = ilmSummaryExtension(indexWithLifecyclePolicy, getUrlForApp); + expect(extension).toBeDefined(); + const rendered = mountWithIntl(extension); + expect(rendered).toMatchSnapshot(); + }); + + test('should return extension when index has lifecycle error', () => { + const extension = ilmSummaryExtension(indexWithLifecycleError, getUrlForApp); + expect(extension).toBeDefined(); + const rendered = mountWithIntl(extension); + expect(rendered).toMatchSnapshot(); + }); + }); + + describe('ilm filter extension', () => { + test('should return empty array when no indices have index lifecycle policy', () => { + const extension = ilmFilterExtension([ + indexWithoutLifecyclePolicy, + indexWithoutLifecyclePolicy, + ]); + expect(extension.length).toBe(0); + }); + + test('should return extension when any index has lifecycle policy', () => { + const extension = ilmFilterExtension([ + indexWithLifecyclePolicy, + indexWithoutLifecyclePolicy, + indexWithoutLifecyclePolicy, + ]); + expect(extension).toBeDefined(); + expect(extension).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 3a19f03547b5b..fb626e7d7fe76 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -10,6 +10,7 @@ import { DeletePhase, HotPhase, WarmPhase, + FrozenPhase, } from '../services/policies/types'; export const defaultNewHotPhase: HotPhase = { @@ -47,6 +48,16 @@ export const defaultNewColdPhase: ColdPhase = { phaseIndexPriority: '0', }; +export const defaultNewFrozenPhase: FrozenPhase = { + phaseEnabled: false, + selectedMinimumAge: '0', + selectedMinimumAgeUnits: 'd', + selectedNodeAttrs: '', + selectedReplicaCount: '', + freezeEnabled: false, + phaseIndexPriority: '0', +}; + export const defaultNewDeletePhase: DeletePhase = { phaseEnabled: false, selectedMinimumAge: '0', 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/components/min_age_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index 11b743ecc4bb6..5128ba1c881a0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -12,7 +12,7 @@ import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -import { ColdPhase, DeletePhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithMinAge, Phases } from '../../../services/policies/types'; function getTimingLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. @@ -27,6 +27,11 @@ function getTimingLabelForPhase(phase: keyof Phases) { defaultMessage: 'Timing for cold phase', }); + case 'frozen': + return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseFrozen.minimumAgeLabel', { + defaultMessage: 'Timing for frozen phase', + }); + case 'delete': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', { defaultMessage: 'Timing for delete phase', @@ -63,7 +68,7 @@ function getUnitsAriaLabelForPhase(phase: keyof Phases) { } } -interface Props { +interface Props { rolloverEnabled: boolean; errors?: PhaseValidationErrors; phase: keyof Phases & string; @@ -72,7 +77,7 @@ interface Props { isShowingErrors: boolean; } -export const MinAgeInput = ({ +export const MinAgeInput = ({ rolloverEnabled, errors, phaseData, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 0ce2c0d7ea566..b4ff62bfb03dc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -20,7 +20,7 @@ import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { useLoadNodes } from '../../../services/api'; import { NodeAttrsDetails } from './node_attrs_details'; -import { ColdPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithAllocationAction, Phases } from '../../../services/policies/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; const learnMoreLink = ( @@ -38,14 +38,14 @@ const learnMoreLink = ( ); -interface Props { +interface Props { phase: keyof Phases & string; errors?: PhaseValidationErrors; phaseData: T; setPhaseData: (dataKey: keyof T & string, value: string) => void; isShowingErrors: boolean; } -export const NodeAllocation = ({ +export const NodeAllocation = ({ phase, setPhaseData, errors, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index 1da7508049f24..1505532a2b16e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -10,17 +10,17 @@ import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eu import { LearnMoreLink } from './'; import { OptionalLabel } from './'; import { ErrableFormRow } from './'; -import { ColdPhase, HotPhase, Phase, Phases, WarmPhase } from '../../../services/policies/types'; +import { PhaseWithIndexPriority, Phases } from '../../../services/policies/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -interface Props { +interface Props { errors?: PhaseValidationErrors; phase: keyof Phases & string; phaseData: T; setPhaseData: (dataKey: keyof T & string, value: any) => void; isShowingErrors: boolean; } -export const SetPriorityInput = ({ +export const SetPriorityInput = ({ errors, phaseData, phase, 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/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index c99d01b546679..db58c64a8ae8c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -28,7 +28,7 @@ import { import { toasts } from '../../services/notification'; -import { Policy, PolicyFromES } from '../../services/policies/types'; +import { Phases, Policy, PolicyFromES } from '../../services/policies/types'; import { validatePolicy, ValidationErrors, @@ -42,7 +42,7 @@ import { } from '../../services/policies/policy_serialization'; import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; -import { ColdPhase, DeletePhase, HotPhase, WarmPhase } from './phases'; +import { ColdPhase, DeletePhase, FrozenPhase, HotPhase, WarmPhase } from './phases'; interface Props { policies: PolicyFromES[]; @@ -118,7 +118,7 @@ export const EditPolicy: React.FunctionComponent = ({ setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout); }; - const setPhaseData = (phase: 'hot' | 'warm' | 'cold' | 'delete', key: string, value: any) => { + const setPhaseData = (phase: keyof Phases, key: string, value: any) => { setPolicy({ ...policy, phases: { @@ -303,6 +303,16 @@ export const EditPolicy: React.FunctionComponent = ({ + 0} + setPhaseData={(key, value) => setPhaseData('frozen', key, value)} + phaseData={policy.phases.frozen} + hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} + /> + + + 0} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index fb32752fe24ea..9df6da7a88b2f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { ColdPhase as ColdPhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { LearnMoreLink, @@ -36,9 +36,8 @@ const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeInd defaultMessage: 'Freeze index', }); -const coldProperty = propertyof('cold'); -const phaseProperty = (propertyName: keyof ColdPhaseInterface) => - propertyof(propertyName); +const coldProperty: keyof Phases = 'cold'; +const phaseProperty = (propertyName: keyof ColdPhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof ColdPhaseInterface & string, value: string | boolean) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx index d3c73090f25f2..eab93777a72bd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; import { DeletePhase as DeletePhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { ActiveBadge, @@ -20,9 +20,8 @@ import { SnapshotPolicies, } from '../components'; -const deleteProperty = propertyof('delete'); -const phaseProperty = (propertyName: keyof DeletePhaseInterface) => - propertyof(propertyName); +const deleteProperty: keyof Phases = 'delete'; +const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx new file mode 100644 index 0000000000000..782906a56a9ba --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx @@ -0,0 +1,210 @@ +/* + * 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, { PureComponent, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFieldNumber, + EuiDescribedFormGroup, + EuiSwitch, + EuiTextColor, +} from '@elastic/eui'; + +import { FrozenPhase as FrozenPhaseInterface, Phases } from '../../../services/policies/types'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; + +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + OptionalLabel, + ErrableFormRow, + MinAgeInput, + NodeAllocation, + SetPriorityInput, +} from '../components'; + +const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', +}); + +const frozenProperty: keyof Phases = 'frozen'; +const phaseProperty = (propertyName: keyof FrozenPhaseInterface) => propertyName; + +interface Props { + setPhaseData: (key: keyof FrozenPhaseInterface & string, value: string | boolean) => void; + phaseData: FrozenPhaseInterface; + isShowingErrors: boolean; + errors?: PhaseValidationErrors; + hotPhaseRolloverEnabled: boolean; +} +export class FrozenPhase extends PureComponent { + render() { + const { + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, + } = this.props; + + return ( +
    + +

    + +

    {' '} + {phaseData.phaseEnabled && !isShowingErrors ? : null} + +
    + } + titleSize="s" + description={ + +

    + +

    + + } + id={`${frozenProperty}-${phaseProperty('phaseEnabled')}`} + checked={phaseData.phaseEnabled} + onChange={(e) => { + setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); + }} + aria-controls="frozenPhaseContent" + /> +
    + } + fullWidth + > + + {phaseData.phaseEnabled ? ( + + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + + + + phase={frozenProperty} + setPhaseData={setPhaseData} + errors={errors} + phaseData={phaseData} + isShowingErrors={isShowingErrors} + /> + + + + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.freezeEnabled} + helpText={i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.replicaCountHelpText', + { + defaultMessage: 'By default, the number of replicas remains the same.', + } + )} + > + { + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); + }} + min={0} + /> + + + + + ) : ( +
    + )} + + + {phaseData.phaseEnabled ? ( + + + +

    + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + > + { + setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); + }} + label={freezeLabel} + aria-label={freezeLabel} + /> + + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + /> + + ) : null} +

    + ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index 22f0114d16afe..106e3b9139a9b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { HotPhase as HotPhaseInterface, Phases } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { LearnMoreLink, @@ -112,9 +112,8 @@ const maxAgeUnits = [ }), }, ]; -const hotProperty = propertyof('hot'); -const phaseProperty = (propertyName: keyof HotPhaseInterface) => - propertyof(propertyName); +const hotProperty: keyof Phases = 'hot'; +const phaseProperty = (propertyName: keyof HotPhaseInterface) => propertyName; interface Props { errors?: PhaseValidationErrors; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts index 8d1ace5950497..d59f2ff6413fd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts @@ -7,4 +7,5 @@ export { HotPhase } from './hot_phase'; export { WarmPhase } from './warm_phase'; export { ColdPhase } from './cold_phase'; +export { FrozenPhase } from './frozen_phase'; export { DeletePhase } from './delete_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index f7b8c60a5c71f..2733d01ac222d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -30,7 +30,7 @@ import { } from '../components'; import { Phases, WarmPhase as WarmPhaseInterface } from '../../../services/policies/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { defaultMessage: 'Shrink index', @@ -47,9 +47,8 @@ const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.force defaultMessage: 'Force merge data', }); -const warmProperty = propertyof('warm'); -const phaseProperty = (propertyName: keyof WarmPhaseInterface) => - propertyof(propertyName); +const warmProperty: keyof Phases = 'warm'; +const phaseProperty = (propertyName: keyof WarmPhaseInterface) => propertyName; interface Props { setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx new file mode 100644 index 0000000000000..90ac3c03856de --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx @@ -0,0 +1,243 @@ +/* + * 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 { get, find } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCallOut, + EuiSelect, + EuiForm, + EuiFormRow, + EuiOverlayMask, + EuiConfirmModal, + EuiFieldText, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +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'; + +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, onCancel } = this.props; + const { templateName, aliasName } = this.state; + const policyName = policy.name; + if (!templateName) { + this.setState({ + templateError: i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.noTemplateSelectedErrorMessage', + { defaultMessage: 'You must select an index template.' } + ), + }); + return; + } + try { + await addLifecyclePolicyToTemplate({ + policyName, + templateName, + aliasName, + }); + const message = i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.successMessage', + { + defaultMessage: 'Added policy {policyName} to index template {templateName}', + values: { policyName, templateName }, + } + ); + toasts.addSuccess(message); + onCancel(); + } catch (e) { + const title = i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.errorMessage', + { + defaultMessage: 'Error adding policy "{policyName}" to index template {templateName}', + values: { policyName, templateName }, + } + ); + showApiError(e, title); + } + }; + renderTemplateHasPolicyWarning() { + const selectedTemplate = this.getSelectedTemplate(); + const existingPolicyName = get(selectedTemplate, 'settings.index.lifecycle.name'); + if (!existingPolicyName) { + return; + } + return ( + + + } + color="warning" + > + + + + + ); + } + getSelectedTemplate() { + const { templates, templateName } = this.state; + return find(templates, (template) => template.name === templateName); + } + renderForm() { + const { templates, templateName, templateError } = this.state; + const options = templates.map(({ name }) => { + return { + value: name, + text: name, + }; + }); + options.unshift({ + value: '', + text: i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateMessage', + { + defaultMessage: 'Select an index template', + } + ), + }); + return ( + + {this.renderTemplateHasPolicyWarning()} + + } + > + { + this.setState({ templateError: undefined, templateName: e.target.value }); + }} + /> + + {this.renderAliasFormElement()} + + ); + } + renderAliasFormElement = () => { + const { aliasName } = this.state; + const { policy } = this.props; + const showAliasTextInput = policy && get(policy, 'policy.phases.hot.actions.rollover'); + if (!showAliasTextInput) { + return null; + } + return ( + + } + > + { + this.setState({ aliasName: e.target.value }); + }} + /> + + ); + }; + render() { + const { policy, onCancel } = this.props; + const title = i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title', + { + defaultMessage: 'Add policy "{name}" to index template', + values: { name: policy.name }, + } + ); + return ( + + + +

    + {' '} + + } + /> +

    +
    + + {this.renderForm()} +
    +
    + ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx new file mode 100644 index 0000000000000..8d8e5ac2a2472 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.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, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; + +import { PolicyFromES } from '../../../services/policies/types'; +import { toasts } from '../../../services/notification'; +import { showApiError } from '../../../services/api_errors'; +import { deletePolicy } from '../../../services/api'; + +interface Props { + policyToDelete: PolicyFromES; + callback: () => void; + onCancel: () => void; +} +export class ConfirmDelete extends Component { + deletePolicy = async () => { + const { policyToDelete, callback } = this.props; + const policyName = policyToDelete.name; + + try { + await deletePolicy(policyName); + const message = i18n.translate('xpack.indexLifecycleMgmt.confirmDelete.successMessage', { + defaultMessage: 'Deleted policy {policyName}', + values: { policyName }, + }); + toasts.addSuccess(message); + } catch (e) { + const title = i18n.translate('xpack.indexLifecycleMgmt.confirmDelete.errorMessage', { + defaultMessage: 'Error deleting policy {policyName}', + values: { policyName }, + }); + showApiError(e, title); + } + if (callback) { + callback(); + } + }; + render() { + const { policyToDelete, onCancel } = this.props; + const title = i18n.translate('xpack.indexLifecycleMgmt.confirmDelete.title', { + defaultMessage: 'Delete policy "{name}"', + values: { name: policyToDelete.name }, + }); + return ( + + + } + confirmButtonText={ + + } + buttonColor="danger" + > +
    + +
    +
    +
    + ); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/index.js deleted file mode 100644 index 2cdbc4a7094a8..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/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 { NoMatch } from './no_match'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/no_match.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/no_match.js deleted file mode 100644 index 45eaaab8a82ae..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/components/no_match/no_match.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 React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const NoMatch = () => ( -
    - -
    -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/index.js deleted file mode 100644 index 63e8cdebd9771..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/no_match/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 { NoMatch } from './components/no_match'; 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/policy_table/add_policy_to_template_confirm_modal.js deleted file mode 100644 index 47134ad097720..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js +++ /dev/null @@ -1,233 +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 { get, find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiCallOut, - EuiSelect, - EuiForm, - EuiFormRow, - EuiOverlayMask, - EuiConfirmModal, - EuiFieldText, - EuiSpacer, - 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'; - -export class AddPolicyToTemplateConfirmModal extends Component { - state = { - templates: [], - }; - async componentDidMount() { - const templates = await loadIndexTemplates(); - this.setState({ templates }); - } - addPolicyToTemplate = async () => { - const { policy, callback, onCancel } = this.props; - const { templateName, aliasName } = this.state; - const policyName = policy.name; - if (!templateName) { - this.setState({ - templateError: i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.noTemplateSelectedErrorMessage', - { defaultMessage: 'You must select an index template.' } - ), - }); - return; - } - try { - await addLifecyclePolicyToTemplate({ - policyName, - templateName, - aliasName, - }); - const message = i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.successMessage', - { - defaultMessage: 'Added policy {policyName} to index template {templateName}', - values: { policyName, templateName }, - } - ); - toasts.addSuccess(message); - onCancel(); - } catch (e) { - const title = i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.errorMessage', - { - defaultMessage: 'Error adding policy "{policyName}" to index template {templateName}', - values: { policyName, templateName }, - } - ); - showApiError(e, title); - } - if (callback) { - callback(); - } - }; - renderTemplateHasPolicyWarning() { - const selectedTemplate = this.getSelectedTemplate(); - const existingPolicyName = get(selectedTemplate, 'settings.index.lifecycle.name'); - if (!existingPolicyName) { - return; - } - return ( - - - } - color="warning" - > - - - - - ); - } - getSelectedTemplate() { - const { templates, templateName } = this.state; - return find(templates, (template) => template.name === templateName); - } - renderForm() { - const { templates, templateName, templateError } = this.state; - const options = templates.map(({ name }) => { - return { - value: name, - text: name, - }; - }); - options.unshift({ - value: '', - text: i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.chooseTemplateMessage', - { - defaultMessage: 'Select an index template', - } - ), - }); - return ( - - {this.renderTemplateHasPolicyWarning()} - - } - > - { - this.setState({ templateError: null, templateName: e.target.value }); - }} - /> - - {this.renderAliasFormElement()} - - ); - } - renderAliasFormElement = () => { - const { aliasName } = this.state; - const { policy } = this.props; - const showAliasTextInput = policy && get(policy, 'policy.phases.hot.actions.rollover'); - if (!showAliasTextInput) { - return null; - } - return ( - - } - > - { - this.setState({ aliasName: e.target.value }); - }} - /> - - ); - }; - render() { - const { policy, onCancel } = this.props; - const title = i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title', - { - defaultMessage: 'Add policy "{name}" to index template', - values: { name: policy.name }, - } - ); - return ( - - - -

    - {' '} - - } - /> -

    -
    - - {this.renderForm()} -
    -
    - ); - } -} 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/policy_table/confirm_delete.js deleted file mode 100644 index 0ecc9cc13ecd0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.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 React, { Component } from 'react'; -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'; - -export class ConfirmDelete extends Component { - deletePolicy = async () => { - const { policyToDelete, callback } = this.props; - const policyName = policyToDelete.name; - - try { - await deletePolicy(policyName); - const message = i18n.translate('xpack.indexLifecycleMgmt.confirmDelete.successMessage', { - defaultMessage: 'Deleted policy {policyName}', - values: { policyName }, - }); - toasts.addSuccess(message); - } catch (e) { - const title = i18n.translate('xpack.indexLifecycleMgmt.confirmDelete.errorMessage', { - defaultMessage: 'Error deleting policy {policyName}', - values: { policyName }, - }); - showApiError(e, title); - } - if (callback) { - callback(); - } - }; - render() { - const { policyToDelete, onCancel } = this.props; - const title = i18n.translate('xpack.indexLifecycleMgmt.confirmDelete.title', { - defaultMessage: 'Delete policy "{name}"', - values: { name: policyToDelete.name }, - }); - return ( - - - } - confirmButtonText={ - - } - 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/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.d.ts deleted file mode 100644 index fa1b1129523eb..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/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 PolicyTable: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.js deleted file mode 100644 index c4aa32f1f7dc2..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/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 { PolicyTable } from './components/policy_table'; 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/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts index 6cc43042ed4ff..7fa82a004b872 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -152,9 +152,9 @@ export const validateColdPhase = (phase: ColdPhase): PhaseValidationErrors { + const phase = { ...frozenPhaseInitialization }; + if (phaseSerialized === undefined || phaseSerialized === null) { + return phase; + } + + phase.phaseEnabled = true; + + if (phaseSerialized.min_age) { + const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); + phase.selectedMinimumAge = minAge; + phase.selectedMinimumAgeUnits = minAgeUnits; + } + + if (phaseSerialized.actions) { + const actions = phaseSerialized.actions; + if (actions.allocate) { + const allocate = actions.allocate; + if (allocate.require) { + Object.entries(allocate.require).forEach((entry) => { + phase.selectedNodeAttrs = entry.join(':'); + }); + if (allocate.number_of_replicas) { + phase.selectedReplicaCount = allocate.number_of_replicas.toString(); + } + } + } + + if (actions.freeze) { + phase.freezeEnabled = true; + } + + if (actions.set_priority) { + phase.phaseIndexPriority = actions.set_priority.priority + ? actions.set_priority.priority.toString() + : ''; + } + } + + return phase; +}; + +export const frozenPhaseToES = ( + phase: FrozenPhase, + originalPhase?: SerializedFrozenPhase +): SerializedFrozenPhase => { + if (!originalPhase) { + originalPhase = { ...serializedPhaseInitialization }; + } + + const esPhase = { ...originalPhase }; + + if (isNumber(phase.selectedMinimumAge)) { + esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; + } + + esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; + + if (phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.require = { + [name]: value, + }; + } else { + if (esPhase.actions.allocate) { + // @ts-expect-error + delete esPhase.actions.allocate.require; + } + } + + if (isNumber(phase.selectedReplicaCount)) { + esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); + esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); + } else { + if (esPhase.actions.allocate) { + // @ts-expect-error + delete esPhase.actions.allocate.number_of_replicas; + } + } + + if ( + esPhase.actions.allocate && + !esPhase.actions.allocate.require && + !isNumber(esPhase.actions.allocate.number_of_replicas) && + isEmpty(esPhase.actions.allocate.include) && + isEmpty(esPhase.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete esPhase.actions.allocate; + } + + if (phase.freezeEnabled) { + esPhase.actions.freeze = {}; + } else { + delete esPhase.actions.freeze; + } + + if (isNumber(phase.phaseIndexPriority)) { + esPhase.actions.set_priority = { + priority: parseInt(phase.phaseIndexPriority, 10), + }; + } else { + delete esPhase.actions.set_priority; + } + + return esPhase; +}; + +export const validateFrozenPhase = (phase: FrozenPhase): PhaseValidationErrors => { + if (!phase.phaseEnabled) { + return {}; + } + + const phaseErrors = {} as PhaseValidationErrors; + + // index priority is optional, but if it's set, it needs to be a positive number + if (phase.phaseIndexPriority) { + if (!isNumber(phase.phaseIndexPriority)) { + phaseErrors.phaseIndexPriority = [numberRequiredMessage]; + } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { + phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; + } + } + + // min age needs to be a positive number + if (!isNumber(phase.selectedMinimumAge)) { + phaseErrors.selectedMinimumAge = [numberRequiredMessage]; + } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { + phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; + } + + return { ...phaseErrors }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts index 3953521df1817..807a6fe8ec395 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -9,6 +9,7 @@ import { defaultNewDeletePhase, defaultNewHotPhase, defaultNewWarmPhase, + defaultNewFrozenPhase, serializedPhaseInitialization, } from '../../constants'; @@ -17,6 +18,7 @@ import { Policy, PolicyFromES, SerializedPolicy } from './types'; import { hotPhaseFromES, hotPhaseToES } from './hot_phase'; import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; +import { frozenPhaseFromES, frozenPhaseToES } from './frozen_phase'; import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; export const splitSizeAndUnits = (field: string): { size: string; units: string } => { @@ -53,6 +55,7 @@ export const initializeNewPolicy = (newPolicyName: string = ''): Policy => { hot: { ...defaultNewHotPhase }, warm: { ...defaultNewWarmPhase }, cold: { ...defaultNewColdPhase }, + frozen: { ...defaultNewFrozenPhase }, delete: { ...defaultNewDeletePhase }, }, }; @@ -70,6 +73,7 @@ export const deserializePolicy = (policy: PolicyFromES): Policy => { hot: hotPhaseFromES(phases.hot), warm: warmPhaseFromES(phases.warm), cold: coldPhaseFromES(phases.cold), + frozen: frozenPhaseFromES(phases.frozen), delete: deletePhaseFromES(phases.delete), }, }; @@ -94,6 +98,13 @@ export const serializePolicy = ( serializedPolicy.phases.cold = coldPhaseToES(policy.phases.cold, originalEsPolicy.phases.cold); } + if (policy.phases.frozen.phaseEnabled) { + serializedPolicy.phases.frozen = frozenPhaseToES( + policy.phases.frozen, + originalEsPolicy.phases.frozen + ); + } + if (policy.phases.delete.phaseEnabled) { serializedPolicy.phases.delete = deletePhaseToES( policy.phases.delete, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts index 545488be2cd5e..6fdbc4babd3f3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -9,7 +9,17 @@ import { validateHotPhase } from './hot_phase'; import { validateWarmPhase } from './warm_phase'; import { validateColdPhase } from './cold_phase'; import { validateDeletePhase } from './delete_phase'; -import { ColdPhase, DeletePhase, HotPhase, Phase, Policy, PolicyFromES, WarmPhase } from './types'; +import { validateFrozenPhase } from './frozen_phase'; + +import { + ColdPhase, + DeletePhase, + FrozenPhase, + HotPhase, + Policy, + PolicyFromES, + WarmPhase, +} from './types'; export const propertyof = (propertyName: keyof T & string) => propertyName; @@ -100,7 +110,7 @@ export const policyNameAlreadyUsedErrorMessage = i18n.translate( defaultMessage: 'That policy name is already used.', } ); -export type PhaseValidationErrors = { +export type PhaseValidationErrors = { [P in keyof Partial]: string[]; }; @@ -108,6 +118,7 @@ export interface ValidationErrors { hot: PhaseValidationErrors; warm: PhaseValidationErrors; cold: PhaseValidationErrors; + frozen: PhaseValidationErrors; delete: PhaseValidationErrors; policyName: string[]; } @@ -148,12 +159,14 @@ export const validatePolicy = ( const hotPhaseErrors = validateHotPhase(policy.phases.hot); const warmPhaseErrors = validateWarmPhase(policy.phases.warm); const coldPhaseErrors = validateColdPhase(policy.phases.cold); + const frozenPhaseErrors = validateFrozenPhase(policy.phases.frozen); const deletePhaseErrors = validateDeletePhase(policy.phases.delete); const isValid = policyNameErrors.length === 0 && Object.keys(hotPhaseErrors).length === 0 && Object.keys(warmPhaseErrors).length === 0 && Object.keys(coldPhaseErrors).length === 0 && + Object.keys(frozenPhaseErrors).length === 0 && Object.keys(deletePhaseErrors).length === 0; return [ isValid, @@ -162,6 +175,7 @@ export const validatePolicy = ( hot: hotPhaseErrors, warm: warmPhaseErrors, cold: coldPhaseErrors, + frozen: frozenPhaseErrors, delete: deletePhaseErrors, }, ]; 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 2e2ed5b38bb87..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; @@ -13,6 +15,7 @@ export interface Phases { hot?: SerializedHotPhase; warm?: SerializedWarmPhase; cold?: SerializedColdPhase; + frozen?: SerializedFrozenPhase; delete?: SerializedDeletePhase; } @@ -21,6 +24,7 @@ export interface PolicyFromES { name: string; policy: SerializedPolicy; version: number; + linkedIndices?: string[]; } export interface SerializedPhase { @@ -68,6 +72,16 @@ export interface SerializedColdPhase extends SerializedPhase { }; } +export interface SerializedFrozenPhase extends SerializedPhase { + actions: { + freeze?: {}; + allocate?: AllocateAction; + set_priority?: { + priority: number | null; + }; + }; +} + export interface SerializedDeletePhase extends SerializedPhase { actions: { wait_for_snapshot?: { @@ -94,47 +108,99 @@ export interface Policy { hot: HotPhase; warm: WarmPhase; cold: ColdPhase; + frozen: FrozenPhase; delete: DeletePhase; }; } -export interface Phase { +export interface CommonPhaseSettings { phaseEnabled: boolean; } -export interface HotPhase extends Phase { + +export interface PhaseWithMinAge { + selectedMinimumAge: string; + selectedMinimumAgeUnits: string; +} + +export interface PhaseWithAllocationAction { + selectedNodeAttrs: string; + selectedReplicaCount: string; +} + +export interface PhaseWithIndexPriority { + phaseIndexPriority: string; +} + +export interface HotPhase extends CommonPhaseSettings, PhaseWithIndexPriority { rolloverEnabled: boolean; selectedMaxSizeStored: string; selectedMaxSizeStoredUnits: string; selectedMaxDocuments: string; selectedMaxAge: string; selectedMaxAgeUnits: string; - phaseIndexPriority: string; } -export interface WarmPhase extends Phase { +export interface WarmPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { warmPhaseOnRollover: boolean; - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; - selectedNodeAttrs: string; - selectedReplicaCount: string; shrinkEnabled: boolean; selectedPrimaryShardCount: string; forceMergeEnabled: boolean; selectedForceMergeSegments: string; - phaseIndexPriority: string; } -export interface ColdPhase extends Phase { - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; - selectedNodeAttrs: string; - selectedReplicaCount: string; +export interface ColdPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { freezeEnabled: boolean; - phaseIndexPriority: string; } -export interface DeletePhase extends Phase { - selectedMinimumAge: string; - selectedMinimumAgeUnits: string; +export interface FrozenPhase + extends CommonPhaseSettings, + PhaseWithMinAge, + PhaseWithAllocationAction, + PhaseWithIndexPriority { + freezeEnabled: boolean; +} + +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/services/ui_metric.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts index 7c7c0b70c0eed..81eb1c8cad135 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts @@ -14,7 +14,6 @@ import { } from '../constants/'; import { getUiMetricsForPhases } from './ui_metric'; -jest.mock('ui/new_platform'); describe('getUiMetricsForPhases', () => { test('gets cold phase', () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/index.js deleted file mode 100644 index fef79c7782bb0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/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 * from './policies'; 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/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js deleted file mode 100644 index 7fe7134f5f5db..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js +++ /dev/null @@ -1,12 +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 { combineReducers } from 'redux'; -import { policies } from './policies'; - -export const indexLifecycleManagement = combineReducers({ - policies, -}); 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/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/index.js deleted file mode 100644 index fef79c7782bb0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/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 * from './policies'; 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.js deleted file mode 100644 index 0bd313c9a9f8d..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js +++ /dev/null @@ -1,289 +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 { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiLink, - EuiSelect, - EuiForm, - EuiFormRow, - EuiOverlayMask, - EuiConfirmModal, - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiCallOut, - EuiSpacer, - EuiModalHeaderTitle, -} from '@elastic/eui'; - -import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api'; -import { showApiError } from '../../application/services/api_errors'; -import { toasts } from '../../application/services/notification'; - -export class AddLifecyclePolicyConfirmModal extends Component { - constructor(props) { - super(props); - this.state = { - policies: [], - selectedPolicyName: '', - selectedAlias: '', - }; - } - addPolicy = async () => { - const { indexName, closeModal, reloadIndices } = this.props; - const { selectedPolicyName, selectedAlias } = this.state; - if (!selectedPolicyName) { - this.setState({ - policyError: i18n.translate( - 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPolicySelectedErrorMessage', - { defaultMessage: 'You must select a policy.' } - ), - }); - return; - } - try { - const body = { - indexName, - policyName: selectedPolicyName, - alias: selectedAlias, - }; - await addLifecyclePolicyToIndex(body); - closeModal(); - toasts.addSuccess( - i18n.translate( - 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.addPolicyToIndexSuccess', - { - defaultMessage: 'Added policy {policyName} to index {indexName}.', - values: { - policyName: selectedPolicyName, - indexName, - }, - } - ) - ); - reloadIndices(); - } catch (err) { - showApiError( - err, - i18n.translate( - 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.addPolicyToIndexError', - { - defaultMessage: 'Error adding policy to index', - } - ) - ); - } - }; - renderAliasFormElement = (selectedPolicy) => { - const { selectedAlias } = this.state; - const { index } = this.props; - const showAliasSelect = - selectedPolicy && get(selectedPolicy, 'policy.phases.hot.actions.rollover'); - if (!showAliasSelect) { - return null; - } - const { aliases } = index; - if (aliases === 'none') { - return ( - - - - } - color="warning" - > - - - - ); - } - const aliasOptions = aliases.map((alias) => { - return { - text: alias, - value: alias, - }; - }); - aliasOptions.unshift({ - text: i18n.translate( - 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.chooseAliasMessage', - { - defaultMessage: 'Choose an alias', - } - ), - value: '', - }); - return ( - - } - > - { - this.setState({ selectedAlias: e.target.value }); - }} - /> - - ); - }; - renderForm() { - const { policies, selectedPolicyName, policyError } = this.state; - const selectedPolicy = selectedPolicyName - ? policies.find((policy) => policy.name === selectedPolicyName) - : null; - - const options = policies.map(({ name }) => { - return { - value: name, - text: name, - }; - }); - options.unshift({ - value: '', - text: i18n.translate( - 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.choosePolicyMessage', - { - defaultMessage: 'Select a lifecycle policy', - } - ), - }); - return ( - - - } - > - { - this.setState({ policyError: null, selectedPolicyName: e.target.value }); - }} - /> - - {this.renderAliasFormElement(selectedPolicy)} - - ); - } - async componentDidMount() { - try { - const policies = await loadPolicies(false, this.props.httpClient); - this.setState({ policies }); - } catch (err) { - showApiError( - err, - i18n.translate( - 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.loadPolicyError', - { - defaultMessage: 'Error loading policy list', - } - ) - ); - this.props.closeModal(); - } - } - render() { - const { policies } = this.state; - const { indexName, closeModal, getUrlForApp } = this.props; - const title = ( - - ); - if (!policies.length) { - return ( - - - - {title} - - - - - } - color="warning" - > -

    - - - -

    -
    -
    -
    -
    - ); - } - return ( - - - } - confirmButtonText={ - - } - > - {this.renderForm()} - - - ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx new file mode 100644 index 0000000000000..060b208006bf3 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx @@ -0,0 +1,307 @@ +/* + * 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 { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ApplicationStart } from 'kibana/public'; + +import { + EuiLink, + EuiSelect, + EuiForm, + EuiFormRow, + EuiOverlayMask, + EuiConfirmModal, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiCallOut, + EuiSpacer, + EuiModalHeaderTitle, +} from '@elastic/eui'; + +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: Props) { + super(props); + this.state = { + policies: [], + selectedPolicyName: '', + selectedAlias: '', + }; + } + addPolicy = async () => { + const { indexName, closeModal, reloadIndices } = this.props; + const { selectedPolicyName, selectedAlias } = this.state; + if (!selectedPolicyName) { + this.setState({ + policyErrorMessage: i18n.translate( + 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPolicySelectedErrorMessage', + { defaultMessage: 'You must select a policy.' } + ), + }); + return; + } + try { + const body = { + indexName, + policyName: selectedPolicyName, + alias: selectedAlias, + }; + await addLifecyclePolicyToIndex(body); + closeModal(); + toasts.addSuccess( + i18n.translate( + 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.addPolicyToIndexSuccess', + { + defaultMessage: 'Added policy {policyName} to index {indexName}.', + values: { + policyName: selectedPolicyName, + indexName, + }, + } + ) + ); + reloadIndices(); + } catch (err) { + showApiError( + err, + i18n.translate( + 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.addPolicyToIndexError', + { + defaultMessage: 'Error adding policy to index', + } + ) + ); + } + }; + renderAliasFormElement = (selectedPolicy?: PolicyFromES) => { + const { selectedAlias } = this.state; + const { index } = this.props; + const showAliasSelect = + selectedPolicy && get(selectedPolicy, 'policy.phases.hot.actions.rollover'); + if (!showAliasSelect) { + return null; + } + const { aliases } = index; + if (aliases === 'none') { + return ( + + + + } + color="warning" + > + + + + ); + } + const aliasOptions = (aliases as string[]).map((alias: string) => { + return { + text: alias, + value: alias, + }; + }); + aliasOptions.unshift({ + text: i18n.translate( + 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.chooseAliasMessage', + { + defaultMessage: 'Choose an alias', + } + ), + value: '', + }); + return ( + + } + > + { + this.setState({ selectedAlias: e.target.value }); + }} + /> + + ); + }; + renderForm() { + const { policies, selectedPolicyName, policyErrorMessage } = this.state; + const selectedPolicy = selectedPolicyName + ? policies.find((policy) => policy.name === selectedPolicyName) + : undefined; + + const options = policies.map(({ name }) => { + return { + value: name, + text: name, + }; + }); + options.unshift({ + value: '', + text: i18n.translate( + 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.choosePolicyMessage', + { + defaultMessage: 'Select a lifecycle policy', + } + ), + }); + return ( + + + } + > + { + this.setState({ policyErrorMessage: undefined, selectedPolicyName: e.target.value }); + }} + /> + + {this.renderAliasFormElement(selectedPolicy)} + + ); + } + async componentDidMount() { + try { + const policies = await loadPolicies(false); + this.setState({ policies }); + } catch (err) { + showApiError( + err, + i18n.translate( + 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.loadPolicyError', + { + defaultMessage: 'Error loading policy list', + } + ) + ); + this.props.closeModal(); + } + } + render() { + const { policies } = this.state; + const { indexName, closeModal, getUrlForApp } = this.props; + const title = ( + + ); + if (!policies.length) { + return ( + + + + {title} + + + + + } + color="warning" + > +

    + + + +

    +
    +
    +
    +
    + ); + } + return ( + + + } + confirmButtonText={ + + } + > + {this.renderForm()} + + + ); + } +} 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.js deleted file mode 100644 index 9e9dc009e4c40..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js +++ /dev/null @@ -1,262 +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 { - EuiButtonEmpty, - EuiCallOut, - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, - EuiSpacer, - EuiTitle, - EuiLink, - EuiPopover, - EuiPopoverTitle, -} from '@elastic/eui'; - -import { getPolicyPath } from '../../application/services/navigation'; - -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', - } - ), - }; -}; -export class IndexLifecycleSummary extends Component { - constructor(props) { - super(props); - this.state = { - showStackPopover: false, - showPhaseExecutionPopover: false, - }; - } - toggleStackPopover = () => { - this.setState({ showStackPopover: !this.state.showStackPopover }); - }; - closeStackPopover = () => { - this.setState({ showStackPopover: false }); - }; - togglePhaseExecutionPopover = () => { - this.setState({ showPhaseExecutionPopover: !this.state.showPhaseExecutionPopover }); - }; - closePhaseExecutionPopover = () => { - this.setState({ showPhaseExecutionPopover: false }); - }; - renderStackPopoverButton(ilm) { - if (!ilm.stack_trace) { - return null; - } - const button = ( - - - - ); - return ( - -
    -
    {ilm.step_info.stack_trace}
    -
    -
    - ); - } - renderPhaseExecutionPopoverButton(ilm) { - if (!ilm.phase_execution) { - return null; - } - const button = ( - - - - ); - return ( - - - - - - - - - - - - {JSON.stringify(ilm.phase_execution, null, 2)} - - - - ); - } - buildRows() { - const { - index: { ilm = {} }, - } = this.props; - const headers = getHeaders(); - const rows = { - left: [], - right: [], - }; - Object.keys(headers).forEach((fieldName, arrayIndex) => { - const value = ilm[fieldName]; - let content; - if (fieldName === 'action_time_millis') { - content = moment(value).format('YYYY-MM-DD HH:mm:ss'); - } else if (fieldName === 'policy') { - content = ( - - {value} - - ); - } else { - content = value; - } - content = content || '-'; - const cell = [ - - {headers[fieldName]} - , - - {content} - , - ]; - if (arrayIndex % 2 === 0) { - rows.left.push(cell); - } else { - rows.right.push(cell); - } - }); - rows.right.push(this.renderPhaseExecutionPopoverButton(ilm)); - return rows; - } - - render() { - const { - index: { ilm = {} }, - } = this.props; - if (!ilm.managed) { - return null; - } - const { left, right } = this.buildRows(); - return ( - - -

    - -

    -
    - {ilm.step_info && ilm.step_info.type ? ( - - - - } - iconType="cross" - > - {ilm.step_info.type}: {ilm.step_info.reason} - - {this.renderStackPopoverButton(ilm)} - - - ) : null} - {ilm.step_info && ilm.step_info.message && !ilm.step_info.stack_trace ? ( - - - - } - > - {ilm.step_info.message} - - - ) : null} - - - - {left} - - - {right} - - -
    - ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx new file mode 100644 index 0000000000000..02e4595a333bc --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx @@ -0,0 +1,293 @@ +/* + * 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 { + EuiButtonEmpty, + EuiCallOut, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiSpacer, + EuiTitle, + EuiLink, + EuiPopover, + 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 = (): 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', + } + ), + ], + ]; +}; + +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, + showPhaseExecutionPopover: false, + }; + } + toggleStackPopover = () => { + this.setState({ showStackPopover: !this.state.showStackPopover }); + }; + closeStackPopover = () => { + this.setState({ showStackPopover: false }); + }; + togglePhaseExecutionPopover = () => { + this.setState({ showPhaseExecutionPopover: !this.state.showPhaseExecutionPopover }); + }; + closePhaseExecutionPopover = () => { + this.setState({ showPhaseExecutionPopover: false }); + }; + renderStackPopoverButton(ilm: IndexLifecyclePolicy) { + if (!ilm.step_info!.stack_trace) { + return null; + } + const button = ( + + + + ); + return ( + +
    +
    {ilm.step_info!.stack_trace}
    +
    +
    + ); + } + renderPhaseExecutionPopoverButton(ilm: IndexLifecyclePolicy) { + const button = ( + + + + ); + return ( + + + + + + + + + + + + {JSON.stringify(ilm.phase_execution, null, 2)} + + + + ); + } + buildRows() { + const { + index: { ilm }, + } = this.props; + const headers = getHeaders(); + const rows: { + left: JSX.Element[]; + right: JSX.Element[]; + } = { + left: [], + right: [], + }; + 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'); + } else if (fieldName === 'policy') { + content = ( + + {value} + + ); + } else { + content = value; + } + content = content || '-'; + const cell = ( + <> + + {label} + + + {content} + + + ); + if (arrayIndex % 2 === 0) { + rows.left.push(cell); + } else { + rows.right.push(cell); + } + }); + if (ilm.phase_execution) { + rows.right.push(this.renderPhaseExecutionPopoverButton(ilm)); + } + return rows; + } + + render() { + const { + index: { ilm }, + } = this.props; + if (!ilm.managed) { + return null; + } + const { left, right } = this.buildRows(); + return ( + <> + +

    + +

    +
    + {ilm.step_info && ilm.step_info.type ? ( + <> + + + } + iconType="cross" + > + {ilm.step_info.type}: {ilm.step_info.reason} + + {this.renderStackPopoverButton(ilm)} + + + ) : null} + {ilm.step_info && ilm.step_info!.message && !ilm.step_info!.stack_trace ? ( + <> + + + } + > + {ilm.step_info!.message} + + + ) : null} + + + + {left} + + + {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.js deleted file mode 100644 index a1eac5264bb6a..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js +++ /dev/null @@ -1,205 +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 from 'react'; -import { get, every, some } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { EuiSearchBar } from '@elastic/eui'; - -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'; - -const stepPath = 'ilm.step'; - -export const retryLifecycleActionExtension = ({ indices }) => { - const allHaveErrors = every(indices, (index) => { - return index.ilm && index.ilm.failed_step; - }); - if (!allHaveErrors) { - return null; - } - const indexNames = indices.map(({ name }) => name); - return { - requestMethod: retryLifecycleForIndex, - icon: 'play', - indexNames: [indexNames], - buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.retryIndexLifecycleActionButtonLabel', { - defaultMessage: 'Retry lifecycle step', - }), - successMessage: i18n.translate( - 'xpack.indexLifecycleMgmt.retryIndexLifecycleAction.retriedLifecycleMessage', - { - defaultMessage: 'Called retry lifecycle step for: {indexNames}', - values: { indexNames: indexNames.map((indexName) => `"${indexName}"`).join(', ') }, - } - ), - }; -}; - -export const removeLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => { - const allHaveIlm = every(indices, (index) => { - return index.ilm && index.ilm.managed; - }); - if (!allHaveIlm) { - return null; - } - const indexNames = indices.map(({ name }) => name); - return { - renderConfirmModal: (closeModal) => { - return ( - - ); - }, - icon: 'stopFilled', - indexNames: [indexNames], - buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.removeIndexLifecycleActionButtonLabel', { - defaultMessage: 'Remove lifecycle policy', - }), - }; -}; - -export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices, getUrlForApp }) => { - if (indices.length !== 1) { - return null; - } - const index = indices[0]; - const hasIlm = index.ilm && index.ilm.managed; - - if (hasIlm) { - return null; - } - const indexName = index.name; - return { - renderConfirmModal: (closeModal) => { - return ( - - ); - }, - icon: 'plusInCircle', - buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel', { - defaultMessage: 'Add lifecycle policy', - }), - }; -}; - -export const ilmBannerExtension = (indices) => { - const { Query } = EuiSearchBar; - if (!indices.length) { - return null; - } - const indicesWithLifecycleErrors = indices.filter((index) => { - return get(index, stepPath) === 'ERROR'; - }); - const numIndicesWithLifecycleErrors = indicesWithLifecycleErrors.length; - if (!numIndicesWithLifecycleErrors) { - return null; - } - return { - type: 'warning', - filter: Query.parse(`${stepPath}:ERROR`), - filterLabel: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.filterLabel', { - defaultMessage: 'Show errors', - }), - title: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.errorMessage', { - defaultMessage: `{ numIndicesWithLifecycleErrors, number} - {numIndicesWithLifecycleErrors, plural, one {index has} other {indices have} } - lifecycle errors`, - values: { numIndicesWithLifecycleErrors }, - }), - }; -}; - -export const ilmSummaryExtension = (index, getUrlForApp) => { - return ; -}; - -export const ilmFilterExtension = (indices) => { - const hasIlm = some(indices, (index) => index.ilm && index.ilm.managed); - if (!hasIlm) { - return []; - } else { - return [ - { - type: 'field_value_selection', - name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecycleStatusLabel', { - defaultMessage: 'Lifecycle status', - }), - multiSelect: false, - field: 'ilm.managed', - options: [ - { - value: true, - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.managedLabel', { - defaultMessage: 'Managed', - }), - }, - { - value: false, - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.unmanagedLabel', { - defaultMessage: 'Unmanaged', - }), - }, - ], - }, - { - type: 'field_value_selection', - field: 'ilm.phase', - name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecyclePhaseLabel', { - defaultMessage: 'Lifecycle phase', - }), - multiSelect: 'or', - options: [ - { - value: 'hot', - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.hotLabel', { - defaultMessage: 'Hot', - }), - }, - { - value: 'warm', - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.warmLabel', { - defaultMessage: 'Warm', - }), - }, - { - value: 'cold', - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', { - defaultMessage: 'Cold', - }), - }, - { - value: 'delete', - view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.deleteLabel', { - defaultMessage: 'Delete', - }), - }, - ], - }, - ]; - } -}; - -export const addAllExtensions = (extensionsService) => { - extensionsService.addAction(retryLifecycleActionExtension); - extensionsService.addAction(removeLifecyclePolicyActionExtension); - extensionsService.addAction(addLifecyclePolicyActionExtension); - - extensionsService.addBanner(ilmBannerExtension); - extensionsService.addSummary(ilmSummaryExtension); - extensionsService.addFilter(ilmFilterExtension); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx new file mode 100644 index 0000000000000..bb5642cf3a476 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx @@ -0,0 +1,235 @@ +/* + * 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 { 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 }: { indices: Index[] }) => { + const allHaveErrors = every(indices, (index) => { + return index.ilm && index.ilm.failed_step; + }); + if (!allHaveErrors) { + return null; + } + const indexNames = indices.map(({ name }: Index) => name); + return { + requestMethod: retryLifecycleForIndex, + icon: 'play', + indexNames: [indexNames], + buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.retryIndexLifecycleActionButtonLabel', { + defaultMessage: 'Retry lifecycle step', + }), + successMessage: i18n.translate( + 'xpack.indexLifecycleMgmt.retryIndexLifecycleAction.retriedLifecycleMessage', + { + defaultMessage: 'Called retry lifecycle step for: {indexNames}', + values: { indexNames: indexNames.map((indexName: string) => `"${indexName}"`).join(', ') }, + } + ), + }; +}; + +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 }: Index) => name); + return { + renderConfirmModal: (closeModal: () => void) => { + return ( + + ); + }, + icon: 'stopFilled', + indexNames: [indexNames], + buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.removeIndexLifecycleActionButtonLabel', { + defaultMessage: 'Remove lifecycle policy', + }), + }; +}; + +export const addLifecyclePolicyActionExtension = ({ + indices, + reloadIndices, + getUrlForApp, +}: { + indices: Index[]; + reloadIndices: () => void; + getUrlForApp: ApplicationStart['getUrlForApp']; +}) => { + if (indices.length !== 1) { + return null; + } + const index = indices[0]; + const hasIlm = index.ilm && index.ilm.managed; + + if (hasIlm) { + return null; + } + const indexName = index.name; + return { + renderConfirmModal: (closeModal: () => void) => { + return ( + + ); + }, + icon: 'plusInCircle', + buttonLabel: i18n.translate('xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel', { + defaultMessage: 'Add lifecycle policy', + }), + }; +}; + +export const ilmBannerExtension = (indices: Index[]) => { + const { Query } = EuiSearchBar; + if (!indices.length) { + return null; + } + const indicesWithLifecycleErrors = indices.filter((index: Index) => { + return get(index, stepPath) === 'ERROR'; + }); + const numIndicesWithLifecycleErrors = indicesWithLifecycleErrors.length; + if (!numIndicesWithLifecycleErrors) { + return null; + } + return { + type: 'warning', + filter: Query.parse(`${stepPath}:ERROR`), + filterLabel: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.filterLabel', { + defaultMessage: 'Show errors', + }), + title: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtBanner.errorMessage', { + defaultMessage: `{ numIndicesWithLifecycleErrors, number} + {numIndicesWithLifecycleErrors, plural, one {index has} other {indices have} } + lifecycle errors`, + values: { numIndicesWithLifecycleErrors }, + }), + }; +}; + +export const ilmSummaryExtension = ( + index: Index, + getUrlForApp: ApplicationStart['getUrlForApp'] +) => { + return ; +}; + +export const ilmFilterExtension = (indices: Index[]) => { + const hasIlm = some(indices, (index) => index.ilm && index.ilm.managed); + if (!hasIlm) { + return []; + } else { + return [ + { + type: 'field_value_selection', + name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecycleStatusLabel', { + defaultMessage: 'Lifecycle status', + }), + multiSelect: false, + field: 'ilm.managed', + options: [ + { + value: true, + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.managedLabel', { + defaultMessage: 'Managed', + }), + }, + { + value: false, + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.unmanagedLabel', { + defaultMessage: 'Unmanaged', + }), + }, + ], + }, + { + type: 'field_value_selection', + field: 'ilm.phase', + name: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.lifecyclePhaseLabel', { + defaultMessage: 'Lifecycle phase', + }), + multiSelect: 'or', + options: [ + { + value: 'hot', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.hotLabel', { + defaultMessage: 'Hot', + }), + }, + { + value: 'warm', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.warmLabel', { + defaultMessage: 'Warm', + }), + }, + { + value: 'frozen', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.frozenLabel', { + defaultMessage: 'Frozen', + }), + }, + { + value: 'cold', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.coldLabel', { + defaultMessage: 'Cold', + }), + }, + { + value: 'delete', + view: i18n.translate('xpack.indexLifecycleMgmt.indexMgmtFilter.deleteLabel', { + defaultMessage: 'Delete', + }), + }, + ], + }, + ]; + } +}; + +export const addAllExtensions = ( + extensionsService: IndexManagementPluginSetup['extensionsService'] +) => { + extensionsService.addAction(retryLifecycleActionExtension); + extensionsService.addAction(removeLifecyclePolicyActionExtension); + extensionsService.addAction(addLifecyclePolicyActionExtension); + + extensionsService.addBanner(ilmBannerExtension); + extensionsService.addSummary(ilmSummaryExtension); + extensionsService.addFilter(ilmFilterExtension); +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index 2d02802119e47..9b51164fd4c28 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -104,6 +104,23 @@ const coldPhaseSchema = schema.maybe( }) ); +const frozenPhaseSchema = schema.maybe( + schema.object({ + min_age: minAgeSchema, + actions: schema.object({ + set_priority: setPrioritySchema, + unfollow: unfollowSchema, + allocate: allocateSchema, + freeze: schema.maybe(schema.object({})), // Freeze has no options + searchable_snapshot: schema.maybe( + schema.object({ + snapshot_repository: schema.string(), + }) + ), + }), + }) +); + const deletePhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, @@ -129,6 +146,7 @@ const bodySchema = schema.object({ hot: hotPhaseSchema, warm: warmPhaseSchema, cold: coldPhaseSchema, + frozen: frozenPhaseSchema, delete: deletePhaseSchema, }), }); 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/component_templates/__jest__/client_integration/component_template_details.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts index a112d73230b82..2a0585d61d6f6 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts @@ -12,11 +12,6 @@ import { ComponentTemplateDeserialized } from '../../shared_imports'; const { setup } = pageHelpers.componentTemplateDetails; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - const COMPONENT_TEMPLATE: ComponentTemplateDeserialized = { name: 'comp-1', template: { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts index bd6ac27375836..9bf7df263ec26 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts @@ -14,11 +14,6 @@ import { API_BASE_PATH } from './helpers/constants'; const { setup } = pageHelpers.componentTemplateList; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: ComponentTemplateListTestBed; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 6b5a848ce85d3..95575124b6abd 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -163,7 +163,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF - {form.isSubmitted && form.isValid === false && ( + {form.isSubmitted && !form.isValid && ( <> {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', { 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/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index fcc9795617ebb..56f040fc59a7b 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -17,13 +17,13 @@ import { i18n } from '@kbn/i18n'; import { useForm, + useFormData, Form, getUseField, getFormRow, Field, Forms, JsonEditorField, - FormDataProvider, } from '../../../../shared_imports'; import { documentationService } from '../../../services/documentation'; import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas'; @@ -118,9 +118,7 @@ interface LogisticsForm { } interface LogisticsFormInternal extends LogisticsForm { - __internal__: { - addMeta: boolean; - }; + addMeta: boolean; } interface Props { @@ -133,14 +131,12 @@ interface Props { function formDeserializer(formData: LogisticsForm): LogisticsFormInternal { return { ...formData, - __internal__: { - addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), - }, + addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), }; } function formSerializer(formData: LogisticsFormInternal): LogisticsForm { - const { __internal__, ...rest } = formData; + const { addMeta, ...rest } = formData; return rest; } @@ -153,7 +149,18 @@ export const StepLogistics: React.FunctionComponent = React.memo( serializer: formSerializer, deserializer: formDeserializer, }); - const { subscribe, submit, isSubmitted, isValid: isFormValid, getErrors: getFormErrors } = form; + const { + submit, + isSubmitted, + isValid: isFormValid, + getErrors: getFormErrors, + getFormData, + } = form; + + const [{ addMeta }] = useFormData({ + form, + watch: 'addMeta', + }); /** * When the consumer call validate() on this step, we submit the form so it enters the "isSubmitted" state @@ -164,15 +171,12 @@ export const StepLogistics: React.FunctionComponent = React.memo( }, [submit]); useEffect(() => { - const subscription = subscribe(({ data, isValid }) => { - onChange({ - isValid, - validate, - getData: data.format, - }); + onChange({ + isValid: isFormValid, + getData: getFormData, + validate, }); - return subscription.unsubscribe; - }, [onChange, validate, subscribe]); + }, [onChange, isFormValid, validate, getFormData]); const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta( documentationService.getEsDocsBase() @@ -296,34 +300,28 @@ export const StepLogistics: React.FunctionComponent = React.memo( defaultMessage="Use the _meta field to store any metadata you want." /> - + } > - - {({ '__internal__.addMeta': addMeta }) => { - return ( - addMeta && ( - - ) - ); - }} - + {addMeta && ( + + )} )} diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 537f421173358..3a03835e85970 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -192,8 +192,8 @@ export const TemplateForm = ({ wizardData: WizardContent ): TemplateDeserialized => { const outputTemplate = { - ...initialTemplate, ...wizardData.logistics, + _kbnMeta: initialTemplate._kbnMeta, composedOf: wizardData.components, template: { settings: wizardData.settings, diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index 0d9ce57a64c84..c85126f08685e 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -125,6 +125,7 @@ export const schemas: Record = { { validator: indexPatternField(i18n), type: VALIDATION_TYPES.ARRAY_ITEM, + isBlocking: false, }, ], }, @@ -213,13 +214,11 @@ export const schemas: Record = { } }, }, - __internal__: { - addMeta: { - type: FIELD_TYPES.TOGGLE, - label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { - defaultMessage: 'Add metadata', - }), - }, + addMeta: { + type: FIELD_TYPES.TOGGLE, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { + defaultMessage: 'Add metadata', + }), }, }, }; 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/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index 16dcab18c3caf..f7f992a090501 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -13,7 +13,7 @@ export { Forms, extractQueryParams, GlobalFlyout, -} from '../../../../src/plugins/es_ui_shared/public/'; +} from '../../../../src/plugins/es_ui_shared/public'; export { FormSchema, @@ -21,6 +21,7 @@ export { VALIDATION_TYPES, FieldConfig, useForm, + useFormData, Form, getUseField, UseField, 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/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index 5077bccdc1ca2..c1d4fc8b8d3c8 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -85,7 +85,7 @@ export const LogEntryRow = memo( ]); const handleOpenViewLogInContext = useCallback(() => { - openViewLogInContext?.(logEntry); // eslint-disable-line no-unused-expressions + openViewLogInContext?.(logEntry); trackMetric({ metric: 'view_in_context__stream' }); }, [openViewLogInContext, logEntry, trackMetric]); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index 028dd0d3a1a7b..740fc8b7bafcd 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -73,7 +73,6 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent { - // eslint-disable-next-line no-unused-expressions services.notifications?.toasts.addError(error, { title: loadDataErrorTitle, }); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index de72ac5c5a574..b33eaf7e77bc3 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -103,7 +103,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { }), }; - // eslint-disable-next-line no-unused-expressions navigateToApp?.('logs', { path: `/stream?${stringify(params)}` }); }, [queryTimeRange, navigateToApp] 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/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts index 8d1b320c89ae6..cd0dd92131230 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -9,6 +9,7 @@ import { Agent, AgentAction, AgentActionSOAttributes } from '../../../common/typ import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { savedObjectToAgentAction } from './saved_objects'; import { appContextService } from '../app_context'; +import { nodeTypes } from '../../../../../../src/plugins/data/common'; export async function createAgentAction( soClient: SavedObjectsClientContract, @@ -29,9 +30,24 @@ export async function getAgentActionsForCheckin( soClient: SavedObjectsClientContract, agentId: string ): Promise { + const filter = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode( + 'not', + nodeTypes.function.buildNode( + 'is', + `${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at`, + '*' + ) + ), + nodeTypes.function.buildNode( + 'is', + `${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.agent_id`, + agentId + ), + ]); const res = await soClient.find({ type: AGENT_ACTION_SAVED_OBJECT_TYPE, - filter: `not ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at: * and ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.agent_id:${agentId}`, + filter, }); return Promise.all( @@ -78,9 +94,26 @@ export async function getAgentActionByIds( } export async function getNewActionsSince(soClient: SavedObjectsClientContract, timestamp: string) { + const filter = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode( + 'not', + nodeTypes.function.buildNode( + 'is', + `${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at`, + '*' + ) + ), + nodeTypes.function.buildNode( + 'range', + `${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.created_at`, + { + gte: timestamp, + } + ), + ]); const res = await soClient.find({ type: AGENT_ACTION_SAVED_OBJECT_TYPE, - filter: `not ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at: * AND ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.created_at >= "${timestamp}"`, + filter, }); return res.saved_objects.map(savedObjectToAgentAction); 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/agents/enroll.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts index 764564cfa49f5..44473bf2d8d79 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts @@ -5,62 +5,52 @@ */ import { validateAgentVersion } from './enroll'; -import { appContextService } from '../app_context'; -import { IngestManagerAppContext } from '../../plugin'; describe('validateAgentVersion', () => { it('should throw with agent > kibana version', () => { - appContextService.start(({ - kibanaVersion: '8.0.0', - } as unknown) as IngestManagerAppContext); - expect(() => - validateAgentVersion({ - local: { elastic: { agent: { version: '8.8.0' } } }, - userProvided: {}, - }) - ).toThrowError(/Agent version is not compatible with kibana version/); + expect(() => validateAgentVersion('8.8.0', '8.0.0')).toThrowError('not compatible'); }); it('should work with agent < kibana version', () => { - appContextService.start(({ - kibanaVersion: '8.0.0', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ local: { elastic: { agent: { version: '7.8.0' } } }, userProvided: {} }); + validateAgentVersion('7.8.0', '8.0.0'); }); it('should work with agent = kibana version', () => { - appContextService.start(({ - kibanaVersion: '8.0.0', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ local: { elastic: { agent: { version: '8.0.0' } } }, userProvided: {} }); + validateAgentVersion('8.0.0', '8.0.0'); }); it('should work with SNAPSHOT version', () => { - appContextService.start(({ - kibanaVersion: '8.0.0-SNAPSHOT', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ - local: { elastic: { agent: { version: '8.0.0-SNAPSHOT' } } }, - userProvided: {}, - }); + validateAgentVersion('8.0.0-SNAPSHOT', '8.0.0-SNAPSHOT'); }); it('should work with a agent using SNAPSHOT version', () => { - appContextService.start(({ - kibanaVersion: '7.8.0', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ - local: { elastic: { agent: { version: '7.8.0-SNAPSHOT' } } }, - userProvided: {}, - }); + validateAgentVersion('7.8.0-SNAPSHOT', '7.8.0'); }); it('should work with a kibana using SNAPSHOT version', () => { - appContextService.start(({ - kibanaVersion: '7.8.0-SNAPSHOT', - } as unknown) as IngestManagerAppContext); - validateAgentVersion({ - local: { elastic: { agent: { version: '7.8.0' } } }, - userProvided: {}, - }); + validateAgentVersion('7.8.0', '7.8.0-SNAPSHOT'); + }); + + it('very close versions, e.g. patch/prerelease - all combos should work', () => { + validateAgentVersion('7.9.1', '7.9.2'); + validateAgentVersion('7.8.1', '7.8.2'); + validateAgentVersion('7.6.99', '7.6.2'); + validateAgentVersion('7.6.2', '7.6.99'); + validateAgentVersion('5.94.3', '5.94.1234-SNAPSHOT'); + validateAgentVersion('5.94.3-SNAPSHOT', '5.94.1'); + }); + + it('somewhat close versions, minor release is 1 or 2 versions back and is older than the stack', () => { + validateAgentVersion('7.9.1', '7.10.2'); + validateAgentVersion('7.9.9', '7.11.1'); + validateAgentVersion('7.6.99', '7.6.2'); + validateAgentVersion('7.6.2', '7.6.99'); + expect(() => validateAgentVersion('5.94.3-SNAPSHOT', '5.93.1')).toThrowError('not compatible'); + expect(() => validateAgentVersion('5.94.3', '5.92.99-SNAPSHOT')).toThrowError('not compatible'); + }); + + it('versions where Agent is a minor version or major version greater (newer) than the stack should not work', () => { + expect(() => validateAgentVersion('7.10.1', '7.9.99')).toThrowError('not compatible'); + expect(() => validateAgentVersion('7.9.9', '6.11.1')).toThrowError('not compatible'); + expect(() => validateAgentVersion('5.94.3', '5.92.99-SNAPSHOT')).toThrowError('not compatible'); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts index 606d5c4dcbb90..3c5850c722e97 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts @@ -20,7 +20,8 @@ export async function enroll( metadata?: { local: any; userProvided: any }, sharedId?: string ): Promise { - validateAgentVersion(metadata); + const agentVersion = metadata?.local?.elastic?.agent?.version; + validateAgentVersion(agentVersion); const existingAgent = sharedId ? await getAgentBySharedId(soClient, sharedId) : null; @@ -89,24 +90,50 @@ async function getAgentBySharedId(soClient: SavedObjectsClientContract, sharedId return null; } -export function validateAgentVersion(metadata?: { local: any; userProvided: any }) { - const kibanaVersion = semver.parse(appContextService.getKibanaVersion()); - if (!kibanaVersion) { - throw Boom.badRequest('Kibana version is not set'); - } - const version = semver.parse(metadata?.local?.elastic?.agent?.version); - if (!version) { - throw Boom.badRequest('Agent version not provided in metadata.'); +export function validateAgentVersion( + agentVersion: string, + kibanaVersion = appContextService.getKibanaVersion() +) { + const agentVersionParsed = semver.parse(agentVersion); + if (!agentVersionParsed) { + throw Boom.badRequest('Agent version not provided'); } - if (!version || !semver.lte(formatVersion(version), formatVersion(kibanaVersion))) { - throw Boom.badRequest('Agent version is not compatible with kibana version'); + const kibanaVersionParsed = semver.parse(kibanaVersion); + if (!kibanaVersionParsed) { + throw Boom.badRequest('Kibana version is not set or provided'); } -} -/** - * used to remove prelease from version as includePrerelease in not working as expected - */ -function formatVersion(version: semver.SemVer) { - return `${version.major}.${version.minor}.${version.patch}`; + const diff = semver.diff(agentVersion, kibanaVersion); + switch (diff) { + // section 1) very close versions, only patch release differences - all combos should work + // Agent a.b.1 < Kibana a.b.2 + // Agent a.b.2 > Kibana a.b.1 + case null: + case 'prerelease': + case 'prepatch': + case 'patch': + return; // OK + + // section 2) somewhat close versions, Agent minor release is 1 or 2 versions back and is older than the stack: + // Agent a.9.x < Kibana a.10.x + // Agent a.9.x < Kibana a.11.x + case 'preminor': + case 'minor': + if ( + agentVersionParsed.minor < kibanaVersionParsed.minor && + kibanaVersionParsed.minor - agentVersionParsed.minor <= 2 + ) + return; + + // section 3) versions where Agent is a minor version or major version greater (newer) than the stack should not work: + // Agent 7.10.x > Kibana 7.9.x + // Agent 8.0.x > Kibana 7.9.x + default: + if (semver.lte(agentVersionParsed, kibanaVersionParsed)) return; + else + throw Boom.badRequest( + `Agent version ${agentVersion} is not compatible with Kibana version ${kibanaVersion}` + ); + } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts index d2a14fcf04dff..e9c8317a6251d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts @@ -3,9 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { pkgToPkgKey } from './index'; const cache: Map = new Map(); export const cacheGet = (key: string) => cache.get(key); export const cacheSet = (key: string, value: Buffer) => cache.set(key, value); export const cacheHas = (key: string) => cache.has(key); -export const getCacheKey = (key: string) => key + '.tar.gz'; +export const cacheClear = () => cache.clear(); +export const cacheDelete = (key: string) => cache.delete(key); + +const archiveLocationCache: Map = new Map(); +export const getArchiveLocation = (name: string, version: string) => + archiveLocationCache.get(pkgToPkgKey({ name, version })); + +export const setArchiveLocation = (name: string, version: string, location: string) => + archiveLocationCache.set(pkgToPkgKey({ name, version }), location); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts index 1f708c5edbcc7..6d029b54a6317 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/extract.ts @@ -5,6 +5,7 @@ */ import tar from 'tar'; +import yauzl from 'yauzl'; import { bufferToStream, streamToBuffer } from './streams'; export interface ArchiveEntry { @@ -30,3 +31,40 @@ export async function untarBuffer( deflatedStream.pipe(inflateStream); }); } + +export async function unzipBuffer( + buffer: Buffer, + filter = (entry: ArchiveEntry): boolean => true, + onEntry = (entry: ArchiveEntry): void => {} +): Promise { + const zipfile = await yauzlFromBuffer(buffer, { lazyEntries: true }); + zipfile.readEntry(); + zipfile.on('entry', async (entry: yauzl.Entry) => { + const path = entry.fileName; + if (!filter({ path })) return zipfile.readEntry(); + + const entryBuffer = await getZipReadStream(zipfile, entry).then(streamToBuffer); + onEntry({ buffer: entryBuffer, path }); + zipfile.readEntry(); + }); + return new Promise((resolve, reject) => zipfile.on('end', resolve).on('error', reject)); +} + +function yauzlFromBuffer(buffer: Buffer, opts: yauzl.Options): Promise { + return new Promise((resolve, reject) => + yauzl.fromBuffer(buffer, opts, (err?: Error, handle?: yauzl.ZipFile) => + err ? reject(err) : resolve(handle) + ) + ); +} + +function getZipReadStream( + zipfile: yauzl.ZipFile, + entry: yauzl.Entry +): Promise { + return new Promise((resolve, reject) => + zipfile.openReadStream(entry, (err?: Error, readStream?: NodeJS.ReadableStream) => + err ? reject(err) : resolve(readStream) + ) + ); +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts index 085dc990fa376..b40638eefbae2 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts @@ -5,7 +5,17 @@ */ import { AssetParts } from '../../../types'; -import { pathParts, splitPkgKey } from './index'; +import { getBufferExtractor, pathParts, splitPkgKey } from './index'; +import { getArchiveLocation } from './cache'; +import { untarBuffer, unzipBuffer } from './extract'; + +jest.mock('./cache', () => { + return { + getArchiveLocation: jest.fn(), + }; +}); + +const mockedGetArchiveLocation = getArchiveLocation as jest.Mock; const testPaths = [ { @@ -80,3 +90,21 @@ describe('splitPkgKey tests', () => { expect(pkgVersion).toBe('0.13.0-alpha.1+abcd'); }); }); + +describe('getBufferExtractor', () => { + it('throws if the archive has not been downloaded/cached yet', () => { + expect(() => getBufferExtractor('missing', '1.2.3')).toThrow('no archive location'); + }); + + it('returns unzipBuffer if the archive key ends in .zip', () => { + mockedGetArchiveLocation.mockImplementation(() => '.zip'); + const extractor = getBufferExtractor('will-use-mocked-key', 'a.b.c'); + expect(extractor).toBe(unzipBuffer); + }); + + it('returns untarBuffer if the key ends in anything else', () => { + mockedGetArchiveLocation.mockImplementation(() => 'xyz'); + const extractor = getBufferExtractor('will-use-mocked-key', 'a.b.c'); + expect(extractor).toBe(untarBuffer); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index b635378960468..61c8cd4aabb7b 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -17,8 +17,8 @@ import { RegistrySearchResults, RegistrySearchResult, } from '../../../types'; -import { cacheGet, cacheSet, getCacheKey, cacheHas } from './cache'; -import { ArchiveEntry, untarBuffer } from './extract'; +import { cacheGet, cacheSet, cacheHas, getArchiveLocation, setArchiveLocation } from './cache'; +import { ArchiveEntry, untarBuffer, unzipBuffer } from './extract'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; import { getRegistryUrl } from './registry_url'; @@ -130,7 +130,9 @@ export async function getArchiveInfo( filter = (entry: ArchiveEntry): boolean => true ): Promise { const paths: string[] = []; - const onEntry = (entry: ArchiveEntry) => { + const archiveBuffer = await getOrFetchArchiveBuffer(pkgName, pkgVersion); + const bufferExtractor = getBufferExtractor(pkgName, pkgVersion); + await bufferExtractor(archiveBuffer, filter, (entry: ArchiveEntry) => { const { path, buffer } = entry; const { file } = pathParts(path); if (!file) return; @@ -138,9 +140,7 @@ export async function getArchiveInfo( cacheSet(path, buffer); paths.push(path); } - }; - - await extract(pkgName, pkgVersion, filter, onEntry); + }); return paths; } @@ -175,24 +175,20 @@ export function pathParts(path: string): AssetParts { } as AssetParts; } -async function extract( - pkgName: string, - pkgVersion: string, - filter = (entry: ArchiveEntry): boolean => true, - onEntry: (entry: ArchiveEntry) => void -) { - const archiveBuffer = await getOrFetchArchiveBuffer(pkgName, pkgVersion); +export function getBufferExtractor(pkgName: string, pkgVersion: string) { + const archiveLocation = getArchiveLocation(pkgName, pkgVersion); + if (!archiveLocation) throw new Error(`no archive location for ${pkgName} ${pkgVersion}`); + const isZip = archiveLocation.endsWith('.zip'); + const bufferExtractor = isZip ? unzipBuffer : untarBuffer; - return untarBuffer(archiveBuffer, filter, onEntry); + return bufferExtractor; } async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { - // assume .tar.gz for now. add support for .zip if/when we need it - const key = getCacheKey(`${pkgName}-${pkgVersion}`); - let buffer = cacheGet(key); + const key = getArchiveLocation(pkgName, pkgVersion); + let buffer = key && cacheGet(key); if (!buffer) { buffer = await fetchArchiveBuffer(pkgName, pkgVersion); - cacheSet(key, buffer); } if (buffer) { @@ -203,16 +199,21 @@ async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Pro } export async function ensureCachedArchiveInfo(name: string, version: string) { - const pkgkey = getCacheKey(`${name}-${version}`); - if (!cacheHas(pkgkey)) { + const pkgkey = getArchiveLocation(name, version); + if (!pkgkey || !cacheHas(pkgkey)) { await getArchiveInfo(name, version); } } async function fetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); - const registryUrl = getRegistryUrl(); - return getResponseStream(`${registryUrl}${archivePath}`).then(streamToBuffer); + const archiveUrl = `${getRegistryUrl()}${archivePath}`; + const buffer = await getResponseStream(archiveUrl).then(streamToBuffer); + + setArchiveLocation(pkgName, pkgVersion, archivePath); + cacheSet(archivePath, buffer); + + return buffer; } export function getAsset(key: string) { 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/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts index 8d6a83a625651..c0acc39ca35a1 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts @@ -13,11 +13,6 @@ import { PipelineListTestBed } from './helpers/pipelines_list.helpers'; const { setup } = pageHelpers.pipelinesList; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: PipelineListTestBed; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/constants.ts new file mode 100644 index 0000000000000..e75ba56277c5c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/constants.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 { Pipeline } from '../../../../../common/types'; +import { VerboseTestOutput, Document } from '../types'; + +export const PROCESSORS: Pick = { + processors: [ + { + set: { + field: 'field1', + value: 'value1', + }, + }, + ], +}; + +export const DOCUMENTS: Document[] = [ + { + _index: 'index', + _id: 'id1', + _source: { + name: 'foo', + }, + }, + { + _index: 'index', + _id: 'id2', + _source: { + name: 'bar', + }, + }, +]; + +export const SIMULATE_RESPONSE: VerboseTestOutput = { + docs: [ + { + processor_results: [ + { + processor_type: 'set', + status: 'success', + tag: 'some_tag', + doc: { + _index: 'index', + _id: 'id1', + _source: { + name: 'foo', + foo: 'bar', + }, + }, + }, + ], + }, + { + processor_results: [ + { + processor_type: 'set', + status: 'success', + tag: 'some_tag', + doc: { + _index: 'index', + _id: 'id2', + _source: { + name: 'bar', + foo: 'bar', + }, + }, + }, + ], + }, + ], +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/http_requests.helpers.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/http_requests.helpers.ts new file mode 100644 index 0000000000000..541a6853a99b3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/http_requests.helpers.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon, { SinonFakeServer } from 'sinon'; + +type HttpResponse = Record | any[]; + +// Register helpers to mock HTTP Requests +const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const setSimulatePipelineResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', '/api/ingest_pipelines/simulate', [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + + return { + setSimulatePipelineResponse, + }; +}; + +export const initHttpRequests = () => { + const server = sinon.fakeServer.create(); + + server.respondImmediately = true; + + // Define default response for unhandled requests. + // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, + // and we can mock them all with a 200 instead of mocking each one individually. + server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); + + const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + + return { + server, + httpRequestsMockHelpers, + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx new file mode 100644 index 0000000000000..fec3259fa019b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx @@ -0,0 +1,231 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import React from 'react'; +import axios from 'axios'; +import axiosXhrAdapter from 'axios/lib/adapters/xhr'; + +import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mocks'; + +import { LocationDescriptorObject } from 'history'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { usageCollectionPluginMock } from 'src/plugins/usage_collection/public/mocks'; + +import { registerTestBed, TestBed } from '../../../../../../../test_utils'; +import { stubWebWorker } from '../../../../../../../test_utils/stub_web_worker'; + +import { + breadcrumbService, + uiMetricService, + documentationService, + apiService, +} from '../../../services'; + +import { + ProcessorsEditorContextProvider, + Props, + GlobalOnFailureProcessorsEditor, + ProcessorsEditor, +} from '../'; +import { TestPipelineActions } from '../'; + +import { initHttpRequests } from './http_requests.helpers'; + +stubWebWorker(); + +jest.mock('../../../../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../../../../src/plugins/kibana_react/public'); + return { + ...original, + // Mocking CodeEditor, which uses React Monaco under the hood + CodeEditor: (props: any) => ( + { + props.onChange(e.jsonContent); + }} + /> + ), + }; +}); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +jest.mock('react-virtualized', () => { + const original = jest.requireActual('react-virtualized'); + + return { + ...original, + AutoSizer: ({ children }: { children: any }) => ( +
    {children({ height: 500, width: 500 })}
    + ), + }; +}); + +const history = scopedHistoryMock.create(); +history.createHref.mockImplementation((location: LocationDescriptorObject) => { + return `${location.pathname}?${location.search}`; +}); + +const appServices = { + breadcrumbs: breadcrumbService, + metric: uiMetricService, + documentation: documentationService, + api: apiService, + notifications: notificationServiceMock.createSetupContract(), + history, + uiSettings: {}, +}; + +const testBedSetup = registerTestBed( + (props: Props) => ( + + + + + + + + ), + { + doMountAsync: false, + } +); + +export interface SetupResult extends TestBed { + actions: ReturnType; +} + +const createActions = (testBed: TestBed) => { + const { find, component, form } = testBed; + + return { + clickAddDocumentsButton() { + act(() => { + find('addDocumentsButton').simulate('click'); + }); + component.update(); + }, + + async clickViewOutputButton() { + await act(async () => { + find('viewOutputButton').simulate('click'); + }); + component.update(); + }, + + closeTestPipelineFlyout() { + act(() => { + find('euiFlyoutCloseButton').simulate('click'); + }); + component.update(); + }, + + clickProcessorOutputTab() { + act(() => { + find('outputTab').simulate('click'); + }); + component.update(); + }, + + async clickRefreshOutputButton() { + await act(async () => { + find('refreshOutputButton').simulate('click'); + }); + component.update(); + }, + + async clickRunPipelineButton() { + await act(async () => { + find('runPipelineButton').simulate('click'); + }); + component.update(); + }, + + async toggleVerboseSwitch() { + await act(async () => { + form.toggleEuiSwitch('verboseOutputToggle'); + }); + component.update(); + }, + + addDocumentsJson(jsonString: string) { + find('documentsEditor').simulate('change', { + jsonString, + }); + }, + + async clickProcessor(processorSelector: string) { + await act(async () => { + find(`${processorSelector}.manageItemButton`).simulate('click'); + }); + component.update(); + }, + }; +}; + +export const setup = async (props: Props): Promise => { + const testBed = await testBedSetup(props); + return { + ...testBed, + actions: createActions(testBed), + }; +}; + +const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); + +export const setupEnvironment = () => { + // Initialize mock services + uiMetricService.setup(usageCollectionPluginMock.createSetupContract()); + // @ts-ignore + apiService.setup(mockHttpClient, uiMetricService); + + const { server, httpRequestsMockHelpers } = initHttpRequests(); + + return { + server, + httpRequestsMockHelpers, + }; +}; + +type TestSubject = + | 'addDocumentsButton' + | 'testPipelineFlyout' + | 'documentsDropdown' + | 'outputTab' + | 'documentsEditor' + | 'runPipelineButton' + | 'documentsTabContent' + | 'outputTabContent' + | 'verboseOutputToggle' + | 'refreshOutputButton' + | 'viewOutputButton' + | 'pipelineExecutionError' + | 'euiFlyoutCloseButton' + | 'processorStatusIcon' + | 'documentsTab' + | 'manageItemButton' + | 'processorSettingsForm' + | 'configurationTab' + | 'outputTab' + | 'processorOutputTabContent' + | string; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx new file mode 100644 index 0000000000000..339c840bb86f1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx @@ -0,0 +1,240 @@ +/* + * 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 { Pipeline } from '../../../../../common/types'; + +import { VerboseTestOutput, Document } from '../types'; +import { setup, SetupResult, setupEnvironment } from './test_pipeline.helpers'; +import { DOCUMENTS, SIMULATE_RESPONSE, PROCESSORS } from './constants'; + +interface ReqBody { + documents: Document[]; + verbose?: boolean; + pipeline: Pick; +} + +describe('Test pipeline', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + server.restore(); + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + testBed = await setup({ + value: { + ...PROCESSORS, + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + describe('Test pipeline actions', () => { + it('should successfully add sample documents and execute the pipeline', async () => { + const { find, actions, exists } = testBed; + + httpRequestsMockHelpers.setSimulatePipelineResponse(SIMULATE_RESPONSE); + + // Flyout and document dropdown should not be visible + expect(exists('testPipelineFlyout')).toBe(false); + expect(exists('documentsDropdown')).toBe(false); + + // Open flyout + actions.clickAddDocumentsButton(); + + // Flyout should be visible with output tab initially disabled + expect(exists('testPipelineFlyout')).toBe(true); + expect(exists('documentsTabContent')).toBe(true); + expect(exists('outputTabContent')).toBe(false); + expect(find('outputTab').props().disabled).toEqual(true); + + // Add sample documents and click run + actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); + await actions.clickRunPipelineButton(); + + // Verify request + const latestRequest = server.requests[server.requests.length - 1]; + const requestBody: ReqBody = JSON.parse(JSON.parse(latestRequest.requestBody).body); + const { + documents: reqDocuments, + verbose: reqVerbose, + pipeline: { processors: reqProcessors }, + } = requestBody; + + expect(reqDocuments).toEqual(DOCUMENTS); + expect(reqVerbose).toEqual(true); + + // We programatically add a unique tag field when calling the simulate API + // We do not know this value in the test, so we simply check that the field exists + // and only verify the processor configuration + reqProcessors.forEach((processor, index) => { + Object.entries(processor).forEach(([key, value]) => { + const { tag, ...config } = value; + expect(tag).toBeDefined(); + expect(config).toEqual(PROCESSORS.processors[index][key]); + }); + }); + + // Verify output tab is active + expect(find('outputTab').props().disabled).toEqual(false); + expect(exists('documentsTabContent')).toBe(false); + expect(exists('outputTabContent')).toBe(true); + + // Click reload button and verify request + const totalRequests = server.requests.length; + await actions.clickRefreshOutputButton(); + expect(server.requests.length).toBe(totalRequests + 1); + expect(server.requests[server.requests.length - 1].url).toBe( + '/api/ingest_pipelines/simulate' + ); + + // Click verbose toggle and verify request + await actions.toggleVerboseSwitch(); + expect(server.requests.length).toBe(totalRequests + 2); + expect(server.requests[server.requests.length - 1].url).toBe( + '/api/ingest_pipelines/simulate' + ); + }); + + test('should enable the output tab if cached documents exist', async () => { + const { actions, exists } = testBed; + + httpRequestsMockHelpers.setSimulatePipelineResponse(SIMULATE_RESPONSE); + + // Open flyout + actions.clickAddDocumentsButton(); + + // Add sample documents and click run + actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); + await actions.clickRunPipelineButton(); + + // Close flyout + actions.closeTestPipelineFlyout(); + expect(exists('testPipelineFlyout')).toBe(false); + expect(exists('addDocumentsButton')).toBe(false); + expect(exists('documentsDropdown')).toBe(true); + + // Reopen flyout and verify output tab is enabled + await actions.clickViewOutputButton(); + expect(exists('testPipelineFlyout')).toBe(true); + expect(exists('documentsTabContent')).toBe(false); + expect(exists('outputTabContent')).toBe(true); + }); + + test('should surface API errors from the request', async () => { + const { actions, find, exists } = testBed; + + const error = { + status: 400, + error: 'Bad Request', + message: + '"[parse_exception] [_source] required property is missing, with { property_name="_source" }"', + }; + + httpRequestsMockHelpers.setSimulatePipelineResponse(undefined, { body: error }); + + // Open flyout + actions.clickAddDocumentsButton(); + + // Add invalid sample documents array and run the pipeline + actions.addDocumentsJson(JSON.stringify([{}])); + await actions.clickRunPipelineButton(); + + // Verify error rendered + expect(exists('pipelineExecutionError')).toBe(true); + expect(find('pipelineExecutionError').text()).toContain(error.message); + }); + }); + + describe('Processors', () => { + // This is a hack + // We need to provide the processor id in the mocked output; + // this is generated dynamically and not something we can stub. + // As a workaround, the value is added as a data attribute in the UI + // and we retrieve it to generate the mocked output. + const addProcessorTagtoMockOutput = (output: VerboseTestOutput) => { + const { find } = testBed; + + const docs = output.docs.map((doc) => { + const results = doc.processor_results.map((result, index) => { + const tag = find(`processors>${index}`).props()['data-processor-id']; + return { + ...result, + tag, + }; + }); + return { processor_results: results }; + }); + return { docs }; + }; + + it('should show "inactive" processor status by default', async () => { + const { find } = testBed; + + const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; + + expect(statusIconLabel).toEqual('Not run'); + }); + + it('should update the processor status after execution', async () => { + const { actions, find } = testBed; + + const mockVerboseOutputWithProcessorTag = addProcessorTagtoMockOutput(SIMULATE_RESPONSE); + httpRequestsMockHelpers.setSimulatePipelineResponse(mockVerboseOutputWithProcessorTag); + + // Open flyout + actions.clickAddDocumentsButton(); + + // Add sample documents and click run + actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); + await actions.clickRunPipelineButton(); + actions.closeTestPipelineFlyout(); + + // Verify status + const statusIconLabel = find('processors>0.processorStatusIcon').props()['aria-label']; + expect(statusIconLabel).toEqual('Success'); + }); + + describe('Output tab', () => { + beforeEach(async () => { + const { actions } = testBed; + + const mockVerboseOutputWithProcessorTag = addProcessorTagtoMockOutput(SIMULATE_RESPONSE); + httpRequestsMockHelpers.setSimulatePipelineResponse(mockVerboseOutputWithProcessorTag); + + // Add documents and run the pipeline + actions.clickAddDocumentsButton(); + actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); + await actions.clickRunPipelineButton(); + actions.closeTestPipelineFlyout(); + }); + + it('should show the output of the processor', async () => { + const { actions, exists } = testBed; + + // Click processor to open manage flyout + await actions.clickProcessor('processors>0'); + // Verify flyout opened + expect(exists('processorSettingsForm')).toBe(true); + + // Navigate to "Output" tab + actions.clickProcessorOutputTab(); + // Verify content + expect(exists('processorOutputTabContent')).toBe(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx index e9aa5c1d56f73..e26b6a2890fe4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx @@ -62,6 +62,7 @@ export const DocumentsDropdown: FunctionComponent = ({ updateSelectedDocument(Number(e.target.value)); }} aria-label={i18nTexts.ariaLabel} + data-test-subj="documentsDropdown" /> 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_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx index c081f69fd41fe..c30fdad969b24 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx @@ -91,7 +91,7 @@ export const ProcessorOutput: React.FunctionComponent = ({ } = processorOutput!; return ( - <> +

    {i18nTexts.tabDescription}

    @@ -212,6 +212,6 @@ export const ProcessorOutput: React.FunctionComponent = ({ )} - +
    ); }; 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/append.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx index 23425297f3420..52750529684b0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx @@ -28,13 +28,13 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Value', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldHelpText', { - defaultMessage: 'The value to be appended by this processor.', + defaultMessage: 'Values to append.', }), validations: [ { validator: emptyField( i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueRequiredError', { - defaultMessage: 'A value to set is required.', + defaultMessage: 'A value is required.', }) ), }, @@ -47,7 +47,7 @@ export const Append: FunctionComponent = () => { <> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx index a76e1a6f3ce9a..6633f9e5de94b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx @@ -17,7 +17,10 @@ export const Bytes: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx index 599d2fdbfd413..70df18acfd0a9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx @@ -5,7 +5,9 @@ */ import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiCode } from '@elastic/eui'; import { FIELD_TYPES, @@ -34,12 +36,15 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Error distance', } ), - helpText: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceHelpText', - { - defaultMessage: - 'The difference between the resulting inscribed distance from center to side and the circle’s radius (measured in meters for geo_shape, unit-less for shape).', - } + helpText: () => ( + {'geo_shape'}, + shape: {'shape'}, + }} + /> ), validations: [ { @@ -66,7 +71,7 @@ const fieldsConfig: FieldsConfig = { }), helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldHelpText', - { defaultMessage: 'Which field mapping type is to be used.' } + { defaultMessage: 'Field mapping type to use when processing the output polygon.' } ), validations: [ { @@ -86,7 +91,7 @@ export const Circle: FunctionComponent = () => { 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..1777cac2a5615 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', { @@ -40,7 +40,7 @@ const ifConfig: FieldConfig = { defaultMessage: 'Condition (optional)', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldHelpText', { - defaultMessage: 'Conditionally execute this processor.', + defaultMessage: 'Conditionally run this processor.', }), type: FIELD_TYPES.TEXT, }; @@ -50,7 +50,7 @@ const tagConfig: FieldConfig = { defaultMessage: 'Tag (optional)', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldHelpText', { - defaultMessage: 'An identifier for this processor. Useful for debugging and metrics.', + defaultMessage: 'Identifier for the processor. Useful for debugging and metrics.', }), type: FIELD_TYPES.TEXT, }; @@ -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/processor_type_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx index e4ad90f61af0a..3264923442886 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx @@ -5,7 +5,7 @@ */ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, ReactNode } from 'react'; import { flow } from 'fp-ts/lib/function'; import { map } from 'fp-ts/lib/Array'; @@ -68,13 +68,18 @@ export const ProcessorTypeField: FunctionComponent = ({ initialType }) => config={typeConfig} defaultValue={initialType} path="type"> {(typeField) => { let selectedOptions: ProcessorTypeAndLabel[]; + let description: string | ReactNode = ''; + if (typeField.value?.length) { const type = typeField.value; - const descriptor = getProcessorDescriptor(type); - selectedOptions = descriptor - ? [{ label: descriptor.label, value: type }] - : // If there is no label for this processor type, just use the type as the label - [{ label: type, value: type }]; + const processorDescriptor = getProcessorDescriptor(type); + if (processorDescriptor) { + description = processorDescriptor.description || ''; + selectedOptions = [{ label: processorDescriptor.label, value: type }]; + } else { + // If there is no label for this processor type, just use the type as the label + selectedOptions = [{ label: type, value: type }]; + } } else { selectedOptions = []; } @@ -102,9 +107,7 @@ export const ProcessorTypeField: FunctionComponent = ({ initialType }) => { @@ -115,14 +115,7 @@ export const Convert: FunctionComponent = () => { path="fields.type" /> - + diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx index 835177dd861d5..471efaa56dea0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx @@ -36,7 +36,7 @@ const isStringLengthOne: ValidationFunc = ({ value }) => { message: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.convertForm.separatorLengthError', { - defaultMessage: 'A separator value must be 1 character.', + defaultMessage: 'Must be a single character.', } ), } @@ -52,7 +52,7 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Target fields', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsHelpText', { - defaultMessage: 'The array of fields to assign extracted values to.', + defaultMessage: 'Output fields. Extracted values are mapped to these fields.', }), validations: [ { @@ -83,7 +83,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {','} }} /> ), @@ -102,7 +102,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'"'} }} /> ), @@ -115,7 +115,7 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Trim', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldHelpText', { - defaultMessage: 'Trim whitespaces in unquoted fields', + defaultMessage: 'Remove whitespaces in unquoted CSV data.', }), }, empty_value: { @@ -127,7 +127,7 @@ const fieldsConfig: FieldsConfig = { 'xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldHelpText', { defaultMessage: - 'Value used to fill empty fields, empty fields will be skipped if this is not provided.', + 'Used to fill empty fields. If no value is provided, empty fields are skipped.', } ), }, @@ -138,7 +138,7 @@ export const CSV: FunctionComponent = () => { <> 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/date.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx index 7e3f8e0d7cd70..8d6d88d2b0662 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx @@ -33,7 +33,7 @@ const fieldsConfig: FieldsConfig = { }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldHelpText', { defaultMessage: - 'An array of the expected date formats. Can be a java time pattern or one of the following formats: ISO8601, UNIX, UNIX_MS, or TAI64N.', + 'Expected date formats. Provided formats are applied sequentially. Accepts a Java time pattern, ISO8601, UNIX, UNIX_MS, or TAI64N formats.', }), validations: [ { @@ -59,7 +59,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'UTC'} }} /> ), @@ -73,7 +73,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'ENGLISH'} }} /> ), @@ -89,7 +89,7 @@ export const DateProcessor: FunctionComponent = () => { @@ -99,7 +99,7 @@ export const DateProcessor: FunctionComponent = () => { helpText={ {'@timestamp'}, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx index 8cbc064c1c90c..73fa54429734f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx @@ -36,7 +36,8 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldHelpText', { - defaultMessage: 'How to round the date when formatting the date into the index name.', + defaultMessage: + 'Time period used to round the date when formatting the date into the index name.', } ), validations: [ @@ -64,7 +65,7 @@ const fieldsConfig: FieldsConfig = { ), helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldHelpText', - { defaultMessage: 'A prefix of the index name to be prepended before the printed date.' } + { defaultMessage: 'Prefix to add before the printed date in the index name.' } ), }, index_name_format: { @@ -79,7 +80,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'yyyy-MM-dd'} }} /> ), @@ -99,7 +100,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {"yyyy-MM-dd'T'HH:mm:ss.SSSXX"} }} /> ), @@ -116,7 +117,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'UTC'} }} /> ), @@ -133,7 +134,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'ENGLISH'} }} /> ), @@ -149,7 +150,7 @@ export const DateIndexName: FunctionComponent = () => { 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..51bc54c5b372c 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 @@ -5,7 +5,7 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiCode } from '@elastic/eui'; +import { EuiCode, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { TextEditor } from '../field_components'; @@ -16,62 +16,88 @@ import { fieldValidators, UseField, Field, + useKibana, } from '../../../../../../shared_imports'; 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; -const fieldsConfig: Record = { - /* Required field config */ - pattern: { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel', { - defaultMessage: 'Pattern', - }), - helpText: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText', - { - defaultMessage: 'The pattern to apply to the field.', - } - ), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternRequiredError', { - defaultMessage: 'A pattern value is required.', - }) - ), - }, - ], - }, - /* Optional field config */ - append_separator: { - type: FIELD_TYPES.TEXT, - label: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel', - { - defaultMessage: 'Append separator (optional)', - } - ), - helpText: ( - {'""'} }} - /> - ), - }, +const getFieldsConfig = (esDocUrl: string): Record => { + return { + /* Required field config */ + pattern: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel', { + defaultMessage: 'Pattern', + }), + helpText: ( + + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText.dissectProcessorLink', + { + defaultMessage: 'key modifier', + } + )} + + ), + }} + /> + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dissectForm.patternRequiredError', + { + defaultMessage: 'A pattern value is required.', + } + ) + ), + }, + ], + }, + /* Optional field config */ + append_separator: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel', + { + defaultMessage: 'Append separator (optional)', + } + ), + helpText: ( + {'""'} }} + /> + ), + }, + }; }; export const Dissect: FunctionComponent = () => { + const { services } = useKibana(); + const fieldsConfig = getFieldsConfig(services.documentation.getEsDocsBasePath()); + return ( <> @@ -80,7 +106,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/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx index 4e50c61ac930c..4f2aa2915fdec 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx @@ -18,7 +18,8 @@ const fieldsConfig: Record = { defaultMessage: 'Path', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathHelpText', { - defaultMessage: 'Only required if the field to expand is part another object field.', + defaultMessage: + 'Output field. Only required if the field to expand is part another object field.', }), }, }; @@ -29,7 +30,7 @@ export const DotExpander: 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/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index 4a67e27d2ebe6..bf69f817183ab 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -141,6 +141,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( alignItems="center" justifyContent="spaceBetween" data-test-subj={selectorToDataTestSubject(selector)} + data-processor-id={processor.id} > diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx index 26ff113b97440..a58d482022b4d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx @@ -79,7 +79,13 @@ export const PipelineProcessorsItemStatus: FunctionComponent = ({ process return ( {label}

    }> - +
    ); }; 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..799551b296bab 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,9 @@ */ import { i18n } from '@kbn/i18n'; -import { FunctionComponent } from 'react'; +import React, { ReactNode } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; import { Append, @@ -28,18 +30,27 @@ 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 */ label: string; + description?: string | ReactNode; } type MapProcessorTypeToDescriptor = Record; @@ -51,6 +62,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.append', { defaultMessage: 'Append', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.append', { + defaultMessage: + "Appends values to a field's array. If the field contains a single value, the processor first converts it to an array. If the field doesn't exist, the processor creates an array containing the appended values.", + }), }, bytes: { FieldsComponent: Bytes, @@ -58,6 +73,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.bytes', { defaultMessage: 'Bytes', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.bytes', { + defaultMessage: + 'Converts digital storage units to bytes. For example, 1KB becomes 1024 bytes.', + }), }, circle: { FieldsComponent: Circle, @@ -65,6 +84,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.circle', { defaultMessage: 'Circle', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.circle', { + defaultMessage: 'Converts a circle definition into an approximate polygon.', + }), }, convert: { FieldsComponent: Convert, @@ -72,6 +94,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.convert', { defaultMessage: 'Convert', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.convert', { + defaultMessage: + 'Converts a field to a different data type. For example, you can convert a string to an long.', + }), }, csv: { FieldsComponent: CSV, @@ -79,6 +105,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.csv', { defaultMessage: 'CSV', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.csv', { + defaultMessage: 'Extracts fields values from CSV data.', + }), }, date: { FieldsComponent: DateProcessor, @@ -86,6 +115,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.date', { defaultMessage: 'Date', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.date', { + defaultMessage: 'Converts a date to a document timestamp.', + }), }, date_index_name: { FieldsComponent: DateIndexName, @@ -93,6 +125,13 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.dateIndexName', { defaultMessage: 'Date index name', }), + description: () => ( + {'my-index-yyyy-MM-dd'} }} + /> + ), }, dissect: { FieldsComponent: Dissect, @@ -100,6 +139,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.dissect', { defaultMessage: 'Dissect', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.dissect', { + defaultMessage: 'Uses dissect patterns to extract matches from a field.', + }), }, dot_expander: { FieldsComponent: DotExpander, @@ -107,6 +149,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.dotExpander', { defaultMessage: 'Dot expander', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.dotExpander', { + defaultMessage: + 'Expands a field containing dot notation into an object field. The object field is then accessible by other processors in the pipeline.', + }), }, drop: { FieldsComponent: Drop, @@ -114,6 +160,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.drop', { defaultMessage: 'Drop', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.drop', { + defaultMessage: + 'Drops documents without returning an error. Used to only index documents that meet specified conditions.', + }), }, enrich: { FieldsComponent: Enrich, @@ -186,63 +236,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 +333,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/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx index 361e32c77d59b..6fd1adad54f84 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx @@ -37,7 +37,7 @@ export const TestOutputButton: FunctionComponent = ({ @@ -51,7 +51,7 @@ export const TestOutputButton: FunctionComponent = ({ {i18nTexts.buttonLabel} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx index e8bb1aa1d357f..b26c6f536366d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx @@ -182,6 +182,7 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ } color="danger" iconType="alert" + data-test-subj="pipelineExecutionError" >

    {testingError.message}

    diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx index 8968416683c3e..dd12cdab0c934 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx @@ -72,7 +72,7 @@ export const DocumentsTab: React.FunctionComponent = ({ }); return ( - <> +

    = ({ path="documents" component={JsonEditorField} componentProps={{ - ['data-test-subj']: 'documentsField', euiCodeEditorProps: { + 'data-test-subj': 'documentsEditor', height: '300px', 'aria-label': i18n.translate( 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldAriaLabel', @@ -128,6 +128,7 @@ export const DocumentsTab: React.FunctionComponent = ({ = ({ )} - +

    ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx index 586fc9e60017a..926bab6da993c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx @@ -56,7 +56,7 @@ export const OutputTab: React.FunctionComponent = ({ } return ( - <> +

    = ({ } checked={isVerboseEnabled} onChange={(e) => onEnableVerbose(e.target.checked)} + data-test-subj="verboseOutputToggle" /> @@ -88,6 +89,7 @@ export const OutputTab: React.FunctionComponent = ({ handleTestPipeline({ documents: cachedDocuments!, verbose: isVerboseEnabled }) } iconType="refresh" + data-test-subj="refreshOutputButton" > = ({ {content} - +

    ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx index d0ea226e8db80..abfb86c2afda1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx @@ -50,7 +50,7 @@ export const Tabs: React.FunctionComponent = ({ isSelected={tab.id === selectedTab} key={tab.id} disabled={getIsDisabled(tab.id)} - data-test-subj={tab.id.toLowerCase() + '_tab'} + data-test-subj={tab.id.toLowerCase() + 'Tab'} > {tab.name} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts index 9b7c2069fcddd..a70c0d281cf95 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts @@ -4,71 +4,143 @@ * you may not use this file except in compliance with the Elastic License. */ -import { deserialize } from './deserialize'; +import { deserialize, deserializeVerboseTestOutput } from './deserialize'; -describe('deserialize', () => { - it('tolerates certain bad values correctly', () => { - expect( - deserialize({ +describe('Deserialization', () => { + describe('deserialize()', () => { + it('tolerates certain bad values correctly', () => { + expect( + deserialize({ + processors: [ + { set: { field: 'test', value: 123 } }, + { badType1: null } as any, + { badType2: 1 } as any, + ], + onFailure: [ + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }) + ).toEqual({ processors: [ - { set: { field: 'test', value: 123 } }, - { badType1: null } as any, - { badType2: 1 } as any, + { + id: expect.any(String), + type: 'set', + options: { + field: 'test', + value: 123, + }, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType1', + options: {}, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType2', + options: {}, + }, ], onFailure: [ { - gsub: { + id: expect.any(String), + type: 'gsub', + onFailure: undefined, + options: { field: '_index', pattern: '(.monitoring-\\w+-)6(-.+)', replacement: '$17$2', }, }, ], - }) - ).toEqual({ - processors: [ + }); + }); + + it('throws for unacceptable values', () => { + expect(() => { + deserialize({ + processors: [{ reallyBad: undefined } as any, 1 as any], + onFailure: [], + }); + }).toThrow('Invalid processor type'); + }); + }); + + describe('deserializeVerboseOutput()', () => { + it('deserializes the verbose output of a simulated pipeline', () => { + expect( + deserializeVerboseTestOutput({ + docs: [ + { + processor_results: [ + { + doc: { + _id: 'id1', + _source: { + name: 'foo', + foo: 'bar', + }, + }, + processor_type: 'set', + status: 'success', + tag: 'e457615c-69c9-4d14-9e85-c477ad96e60f', + }, + ], + }, + { + processor_results: [ + { + doc: { + _id: 'id2', + _source: { + name: 'baz', + foo: 'bar', + }, + }, + processor_type: 'set', + status: 'success', + tag: 'e457615c-69c9-4d14-9e85-c477ad96e60f', + }, + ], + }, + ], + }) + ).toEqual([ { - id: expect.any(String), - type: 'set', - options: { - field: 'test', - value: 123, + 'e457615c-69c9-4d14-9e85-c477ad96e60f': { + doc: { + _id: 'id1', + _source: { + name: 'foo', + foo: 'bar', + }, + }, + processor_type: 'set', + status: 'success', }, }, { - id: expect.any(String), - onFailure: undefined, - type: 'badType1', - options: {}, - }, - { - id: expect.any(String), - onFailure: undefined, - type: 'badType2', - options: {}, - }, - ], - onFailure: [ - { - id: expect.any(String), - type: 'gsub', - onFailure: undefined, - options: { - field: '_index', - pattern: '(.monitoring-\\w+-)6(-.+)', - replacement: '$17$2', + 'e457615c-69c9-4d14-9e85-c477ad96e60f': { + doc: { + _id: 'id2', + _source: { + name: 'baz', + foo: 'bar', + }, + }, + processor_type: 'set', + status: 'success', }, }, - ], + ]); }); }); - - it('throws for unacceptable values', () => { - expect(() => { - deserialize({ - processors: [{ reallyBad: undefined } as any, 1 as any], - onFailure: [], - }); - }).toThrow('Invalid processor type'); - }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts index 9083985b0ff2e..5229f5eb0bb21 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts @@ -90,7 +90,7 @@ export type ProcessorStatus = export interface ProcessorResult { processor_type: string; status: ProcessorStatus; - doc: Document; + doc?: Document; tag: string; ignored_error?: any; error?: any; 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/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 79986986b217d..4e813494d7d32 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -161,7 +161,7 @@ export function PieComponent( // to account for in outer labels // This does not handle non-dashboard embeddables, which are allowed to // have different backgrounds. - textColor: chartTheme.axes?.axisTitleStyle?.fill, + textColor: chartTheme.axes?.axisTitle?.fill, }, sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill, sectorLineWidth: 1.5, 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/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap index f0c233b44a285..79d3528eef4c4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap @@ -20,23 +20,47 @@ exports[`xy_expression XYChart component it renders area 1`] = ` } /> @@ -151,23 +175,47 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` } /> @@ -272,23 +320,47 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` } /> @@ -393,23 +465,47 @@ exports[`xy_expression XYChart component it renders line 1`] = ` } /> @@ -524,23 +620,47 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` } /> @@ -563,6 +683,11 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` ] } enableHistogramMode={false} + fit={ + Object { + "type": "none", + } + } groupId="left" id="d-a" key="0-0" @@ -606,6 +731,11 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` ] } enableHistogramMode={false} + fit={ + Object { + "type": "none", + } + } groupId="left" id="d-b" key="0-1" @@ -653,23 +783,47 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` } /> @@ -782,23 +936,47 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = } /> diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 31ba1bc83d970..e80bb22c04a69 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -107,7 +107,7 @@ describe('XY Config panels', () => { expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry'); }); - it('should disable the select if there is no unstacked area or line series', () => { + it('should disable the select if there is no area or line series', () => { const state = testState(); const component = shallow( { setState={jest.fn()} state={{ ...state, - layers: [ - { ...state.layers[0], seriesType: 'bar' }, - { ...state.layers[0], seriesType: 'area_stacked' }, - ], + layers: [{ ...state.layers[0], seriesType: 'bar' }], fittingFunction: 'Carry', }} /> diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index d64eb9451a50e..a2488d123e13a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -135,8 +135,8 @@ export function XyToolbar(props: VisualizationToolbarProps) { const { frame, state, setState } = props; const [open, setOpen] = useState(false); - const hasNonBarSeries = state?.layers.some( - (layer) => layer.seriesType === 'line' || layer.seriesType === 'area' + const hasNonBarSeries = state?.layers.some(({ seriesType }) => + ['area_stacked', 'area', 'line'].includes(seriesType) ); const [xAxisTitle, setXAxisTitle] = useState(state?.xTitle); @@ -261,8 +261,7 @@ export function XyToolbar(props: VisualizationToolbarProps) { content={ !hasNonBarSeries && i18n.translate('xpack.lens.xyChart.fittingDisabledHelpText', { - defaultMessage: - 'This setting only applies to line charts and unstacked area charts.', + defaultMessage: 'This setting only applies to line and area charts.', }) } > diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index ba1ff6a1df030..c9c27193c437e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -1414,7 +1414,7 @@ describe('xy_expression', () => { expect(convertSpy).toHaveBeenCalledWith('I'); }); - test('it should not pass the formatter function to the x axis if the visibility of the tick labels is off', () => { + test('it should set the tickLabel visibility on the x axis if the tick labels is hidden', () => { const { data, args } = sampleArgs(); args.tickLabelsVisibilitySettings = { x: false, y: true, type: 'lens_xy_tickLabelsConfig' }; @@ -1432,15 +1432,94 @@ describe('xy_expression', () => { /> ); - const tickFormatter = instance.find(Axis).first().prop('tickFormat'); + const axisStyle = instance.find(Axis).first().prop('style'); - if (!tickFormatter) { - throw new Error('tickFormatter prop not found'); - } + expect(axisStyle).toMatchObject({ + tickLabel: { + visible: false, + }, + }); + }); - tickFormatter('I'); + test('it should set the tickLabel visibility on the y axis if the tick labels is hidden', () => { + const { data, args } = sampleArgs(); + + args.tickLabelsVisibilitySettings = { x: true, y: false, type: 'lens_xy_tickLabelsConfig' }; - expect(convertSpy).toHaveBeenCalledTimes(0); + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).at(1).prop('style'); + + expect(axisStyle).toMatchObject({ + tickLabel: { + visible: false, + }, + }); + }); + + test('it should set the tickLabel visibility on the x axis if the tick labels is shown', () => { + const { data, args } = sampleArgs(); + + args.tickLabelsVisibilitySettings = { x: true, y: true, type: 'lens_xy_tickLabelsConfig' }; + + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).first().prop('style'); + + expect(axisStyle).toMatchObject({ + tickLabel: { + visible: true, + }, + }); + }); + + test('it should set the tickLabel visibility on the y axis if the tick labels is shown', () => { + const { data, args } = sampleArgs(); + + args.tickLabelsVisibilitySettings = { x: false, y: true, type: 'lens_xy_tickLabelsConfig' }; + + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).at(1).prop('style'); + + expect(axisStyle).toMatchObject({ + tickLabel: { + visible: true, + }, + }); }); test('it should remove invalid rows', () => { @@ -1766,8 +1845,7 @@ describe('xy_expression', () => { expect(component.find(BarSeries).prop('fit')).toEqual(undefined); expect(component.find(AreaSeries).at(0).prop('fit')).toEqual({ type: Fit.Carry }); expect(component.find(AreaSeries).at(0).prop('stackAccessors')).toEqual([]); - // stacked area series doesn't get the fit prop - expect(component.find(AreaSeries).at(1).prop('fit')).toEqual(undefined); + expect(component.find(AreaSeries).at(1).prop('fit')).toEqual({ type: Fit.Carry }); expect(component.find(AreaSeries).at(1).prop('stackAccessors')).toEqual(['c']); }); @@ -1831,7 +1909,13 @@ describe('xy_expression', () => { /> ); - expect(component.find(Axis).at(0).prop('title')).toEqual(undefined); + const axisStyle = component.find(Axis).first().prop('style'); + + expect(axisStyle).toMatchObject({ + axisTitle: { + visible: false, + }, + }); }); test('it should show the X axis gridlines if the setting is on', () => { @@ -1852,7 +1936,9 @@ describe('xy_expression', () => { /> ); - expect(component.find(Axis).at(0).prop('showGridLines')).toBeTruthy(); + expect(component.find(Axis).at(0).prop('gridLine')).toMatchObject({ + visible: true, + }); }); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 2037a3dbe6623..31873228fb394 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -420,11 +420,21 @@ export function XYChart({ xAxisFormatter.convert(d) : () => ''} + tickFormat={(d) => xAxisFormatter.convert(d)} + style={{ + tickLabel: { + visible: tickLabelsVisibilitySettings?.x, + }, + axisTitle: { + visible: showXAxisTitle, + }, + }} /> {yAxesConfiguration.map((axis, index) => ( @@ -433,10 +443,20 @@ export function XYChart({ id={axis.groupId} groupId={axis.groupId} position={axis.position} - title={showYAxisTitle ? getYAxesTitles(axis.series, index) : undefined} - showGridLines={gridlinesVisibilitySettings?.y} + title={getYAxesTitles(axis.series, index)} + gridLine={{ + visible: gridlinesVisibilitySettings?.y, + }} hide={filteredLayers[0].hide} - tickFormat={tickLabelsVisibilitySettings?.y ? (d) => axis.formatter.convert(d) : () => ''} + tickFormat={(d) => axis.formatter.convert(d)} + style={{ + tickLabel: { + visible: tickLabelsVisibilitySettings?.y, + }, + axisTitle: { + visible: showYAxisTitle, + }, + }} /> ))} @@ -529,7 +549,9 @@ export function XYChart({ case 'bar_horizontal_stacked': return ; case 'area_stacked': - return ; + return ( + + ); case 'area': return ( diff --git a/x-pack/plugins/licensing/public/mocks.ts b/x-pack/plugins/licensing/public/mocks.ts index 8421a343d91ca..1ddde892de0d9 100644 --- a/x-pack/plugins/licensing/public/mocks.ts +++ b/x-pack/plugins/licensing/public/mocks.ts @@ -6,12 +6,14 @@ import { BehaviorSubject } from 'rxjs'; import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { licenseMock } from '../common/licensing.mock'; +import { featureUsageMock } from './services/feature_usage_service.mock'; const createSetupMock = () => { const license = licenseMock.createLicense(); const mock: jest.Mocked = { license$: new BehaviorSubject(license), refresh: jest.fn(), + featureUsage: featureUsageMock.createSetup(), }; mock.refresh.mockResolvedValue(license); @@ -23,6 +25,7 @@ const createStartMock = () => { const mock: jest.Mocked = { license$: new BehaviorSubject(license), refresh: jest.fn(), + featureUsage: featureUsageMock.createStart(), }; mock.refresh.mockResolvedValue(license); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index ec42a73f610c0..aa0c25364f2c7 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -6,12 +6,12 @@ import { Observable, Subject, Subscription } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; - import { ILicense } from '../common/types'; import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { createLicenseUpdate } from '../common/license_update'; import { License } from '../common/license'; import { mountExpiredBanner } from './expired_banner'; +import { FeatureUsageService } from './services'; export const licensingSessionStorageKey = 'xpack.licensing'; @@ -39,6 +39,7 @@ export class LicensingPlugin implements Plugin Promise; private license$?: Observable; + private featureUsage = new FeatureUsageService(); constructor( context: PluginInitializerContext, @@ -116,6 +117,7 @@ export class LicensingPlugin implements Plugin => { + const mock = { + register: jest.fn(), + }; + + mock.register.mockImplementation(() => Promise.resolve()); + + return mock; +}; + +const createStartMock = (): jest.Mocked => { + const mock = { + notifyUsage: jest.fn(), + }; + + mock.notifyUsage.mockImplementation(() => Promise.resolve()); + + return mock; +}; + +const createServiceMock = (): jest.Mocked> => { + const mock = { + setup: jest.fn(), + start: jest.fn(), + }; + + mock.setup.mockImplementation(() => createSetupMock()); + mock.start.mockImplementation(() => createStartMock()); + + return mock; +}; + +export const featureUsageMock = { + create: createServiceMock, + createSetup: createSetupMock, + createStart: createStartMock, +}; diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.test.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.test.ts new file mode 100644 index 0000000000000..eba2d1e67b509 --- /dev/null +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { httpServiceMock } from '../../../../../src/core/public/mocks'; +import { FeatureUsageService } from './feature_usage_service'; + +describe('FeatureUsageService', () => { + let http: ReturnType; + let service: FeatureUsageService; + + beforeEach(() => { + http = httpServiceMock.createSetupContract(); + service = new FeatureUsageService(); + }); + + describe('#setup', () => { + describe('#register', () => { + it('calls the endpoint with the correct parameters', async () => { + const setup = service.setup({ http }); + await setup.register('my-feature', 'platinum'); + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/register', { + body: JSON.stringify({ + featureName: 'my-feature', + licenseType: 'platinum', + }), + }); + }); + }); + }); + + describe('#start', () => { + describe('#notifyUsage', () => { + it('calls the endpoint with the correct parameters', async () => { + service.setup({ http }); + const start = service.start({ http }); + await start.notifyUsage('my-feature', 42); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/notify', { + body: JSON.stringify({ + featureName: 'my-feature', + lastUsed: 42, + }), + }); + }); + + it('correctly convert dates', async () => { + service.setup({ http }); + const start = service.start({ http }); + + const now = new Date(); + + await start.notifyUsage('my-feature', now); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith('/internal/licensing/feature_usage/notify', { + body: JSON.stringify({ + featureName: 'my-feature', + lastUsed: now.getTime(), + }), + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.ts new file mode 100644 index 0000000000000..588d22eeb818d --- /dev/null +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.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 isDate from 'lodash/isDate'; +import type { HttpSetup, HttpStart } from 'src/core/public'; +import { LicenseType } from '../../common/types'; + +/** @public */ +export interface FeatureUsageServiceSetup { + /** + * Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}. + */ + register(featureName: string, licenseType: LicenseType): Promise; +} + +/** @public */ +export interface FeatureUsageServiceStart { + /** + * Notify of a registered feature usage at given time. + * + * @param featureName - the name of the feature to notify usage of + * @param usedAt - Either a `Date` or an unix timestamp with ms. If not specified, it will be set to the current time. + */ + notifyUsage(featureName: string, usedAt?: Date | number): Promise; +} + +interface SetupDeps { + http: HttpSetup; +} + +interface StartDeps { + http: HttpStart; +} + +/** + * @internal + */ +export class FeatureUsageService { + public setup({ http }: SetupDeps): FeatureUsageServiceSetup { + return { + register: async (featureName, licenseType) => { + await http.post('/internal/licensing/feature_usage/register', { + body: JSON.stringify({ + featureName, + licenseType, + }), + }); + }, + }; + } + + public start({ http }: StartDeps): FeatureUsageServiceStart { + return { + notifyUsage: async (featureName, usedAt = Date.now()) => { + const lastUsed = isDate(usedAt) ? usedAt.getTime() : usedAt; + await http.post('/internal/licensing/feature_usage/notify', { + body: JSON.stringify({ + featureName, + lastUsed, + }), + }); + }, + }; + } +} diff --git a/x-pack/plugins/licensing/public/services/index.ts b/x-pack/plugins/licensing/public/services/index.ts new file mode 100644 index 0000000000000..fc890dd3c927d --- /dev/null +++ b/x-pack/plugins/licensing/public/services/index.ts @@ -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 { + FeatureUsageService, + FeatureUsageServiceSetup, + FeatureUsageServiceStart, +} from './feature_usage_service'; diff --git a/x-pack/plugins/licensing/public/types.ts b/x-pack/plugins/licensing/public/types.ts index 71a4a452d163d..43b146c51d9a8 100644 --- a/x-pack/plugins/licensing/public/types.ts +++ b/x-pack/plugins/licensing/public/types.ts @@ -6,6 +6,7 @@ import { Observable } from 'rxjs'; import { ILicense } from '../common/types'; +import { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; /** @public */ export interface LicensingPluginSetup { @@ -19,6 +20,10 @@ export interface LicensingPluginSetup { * @deprecated in favour of the counterpart provided from start contract */ refresh(): Promise; + /** + * APIs to register licensed feature usage. + */ + featureUsage: FeatureUsageServiceSetup; } /** @public */ @@ -31,4 +36,8 @@ export interface LicensingPluginStart { * Triggers licensing information re-fetch. */ refresh(): Promise; + /** + * APIs to manage licensed feature usage. + */ + featureUsage: FeatureUsageServiceStart; } diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 6cdba0ac46644..2ee8d26419571 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -133,7 +133,9 @@ export class LicensingPlugin implements Plugin ) { registerInfoRoute(router); registerFeatureUsageRoute(router, getStartServices); + registerRegisterFeatureRoute(router, featureUsageSetup); + registerNotifyFeatureUsageRoute(router); } diff --git a/x-pack/plugins/licensing/server/routes/internal/index.ts b/x-pack/plugins/licensing/server/routes/internal/index.ts new file mode 100644 index 0000000000000..a3b06c223fc12 --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/internal/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 { registerNotifyFeatureUsageRoute } from './notify_feature_usage'; +export { registerRegisterFeatureRoute } from './register_feature'; diff --git a/x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts b/x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts new file mode 100644 index 0000000000000..ec70472574be3 --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/internal/notify_feature_usage.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export function registerNotifyFeatureUsageRoute(router: IRouter) { + router.post( + { + path: '/internal/licensing/feature_usage/notify', + validate: { + body: schema.object({ + featureName: schema.string(), + lastUsed: schema.number(), + }), + }, + }, + async (context, request, response) => { + const { featureName, lastUsed } = request.body; + + context.licensing.featureUsage.notifyUsage(featureName, lastUsed); + + return response.ok({ + body: { + success: true, + }, + }); + } + ); +} diff --git a/x-pack/plugins/licensing/server/routes/internal/register_feature.ts b/x-pack/plugins/licensing/server/routes/internal/register_feature.ts new file mode 100644 index 0000000000000..14f7952f86f5a --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/internal/register_feature.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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { LicenseType, LICENSE_TYPE } from '../../../common/types'; +import { FeatureUsageServiceSetup } from '../../services'; + +export function registerRegisterFeatureRoute( + router: IRouter, + featureUsageSetup: FeatureUsageServiceSetup +) { + router.post( + { + path: '/internal/licensing/feature_usage/register', + validate: { + body: schema.object({ + featureName: schema.string(), + licenseType: schema.string({ + validate: (value) => { + if (!(value in LICENSE_TYPE)) { + return `Invalid license type: ${value}`; + } + }, + }), + }), + }, + }, + async (context, request, response) => { + const { featureName, licenseType } = request.body; + + featureUsageSetup.register(featureName, licenseType as LicenseType); + + return response.ok({ + body: { + success: true, + }, + }); + } + ); +} diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts index 39f7aa6503b35..943516dc0409a 100644 --- a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts @@ -19,12 +19,22 @@ describe('FeatureUsageService', () => { describe('#setup', () => { describe('#register', () => { - it('throws when registering the same feature twice', () => { + it('does not throw when registering the same feature twice with the same license', () => { const setup = service.setup(); setup.register('foo', 'basic'); expect(() => { setup.register('foo', 'basic'); - }).toThrowErrorMatchingInlineSnapshot(`"Feature 'foo' has already been registered."`); + }).not.toThrow(); + }); + + it('throws when registering the same feature again with a different license', () => { + const setup = service.setup(); + setup.register('foo', 'basic'); + expect(() => { + setup.register('foo', 'enterprise'); + }).toThrowErrorMatchingInlineSnapshot( + `"Feature 'foo' has already been registered with another license type. (current: basic, new: enterprise)"` + ); }); }); }); diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.ts index 9bfcb28f36b2a..75a0c7fa78108 100644 --- a/x-pack/plugins/licensing/server/services/feature_usage_service.ts +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.ts @@ -43,14 +43,20 @@ export class FeatureUsageService { public setup(): FeatureUsageServiceSetup { return { register: (featureName, licenseType) => { - if (this.lastUsages.has(featureName)) { - throw new Error(`Feature '${featureName}' has already been registered.`); + const registered = this.lastUsages.get(featureName); + if (registered) { + if (registered.licenseType !== licenseType) { + throw new Error( + `Feature '${featureName}' has already been registered with another license type. (current: ${registered.licenseType}, new: ${licenseType})` + ); + } + } else { + this.lastUsages.set(featureName, { + name: featureName, + lastUsed: null, + licenseType, + }); } - this.lastUsages.set(featureName, { - name: featureName, - lastUsed: null, - licenseType, - }); }, }; } 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/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json new file mode 100644 index 0000000000000..9f0c306a408f0 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json @@ -0,0 +1,18 @@ +{ + "list_id": "endpoint_trusted_apps", + "item_id": "endpoint_trusted_apps_item", + "_tags": ["endpoint", "os:linux", "os:windows", "os:macos", "trusted-app"], + "tags": ["user added string for a tag", "malware"], + "type": "simple", + "description": "This is a sample agnostic endpoint trusted app entry", + "name": "Sample Endpoint Trusted App Entry", + "namespace_type": "agnostic", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "included", + "type": "match", + "value": "Elastic, N.V." + } + ] +} diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js index 694e50cfe3e36..6cb1f87648da1 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js @@ -125,11 +125,11 @@ class PipelineEditorUi extends React.Component { onPipelineSave = () => { const { pipelineService, toastNotifications, intl } = this.props; - const { id } = this.state.pipeline; + const { id, ...pipelineToStore } = this.state.pipeline; return pipelineService .savePipeline({ id, - upstreamJSON: this.state.pipeline, + upstreamJSON: pipelineToStore, }) .then(() => { toastNotifications.addSuccess( diff --git a/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts b/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts index 8ce04c83afdbf..0b7c3888b6f03 100755 --- a/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts +++ b/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts @@ -11,14 +11,14 @@ import { i18n } from '@kbn/i18n'; interface PipelineOptions { id: string; - description: string; + description?: string; pipeline: string; username?: string; settings?: Record; } interface DownstreamPipeline { - description: string; + description?: string; pipeline: string; settings?: Record; } @@ -27,7 +27,7 @@ interface DownstreamPipeline { */ export class Pipeline { public readonly id: string; - public readonly description: string; + public readonly description?: string; public readonly username?: string; public readonly pipeline: string; private readonly settings: Record; diff --git a/x-pack/plugins/logstash/server/routes/pipeline/save.ts b/x-pack/plugins/logstash/server/routes/pipeline/save.ts index e484d0e221b6d..755a82e670a2a 100644 --- a/x-pack/plugins/logstash/server/routes/pipeline/save.ts +++ b/x-pack/plugins/logstash/server/routes/pipeline/save.ts @@ -22,8 +22,7 @@ export function registerPipelineSaveRoute(router: IRouter, security?: SecurityPl id: schema.string(), }), body: schema.object({ - id: schema.string(), - description: schema.string(), + description: schema.maybe(schema.string()), pipeline: schema.string(), settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 363122ac62212..a4f20caedfc9b 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -33,6 +33,12 @@ export const MAP_PATH = 'map'; export const GIS_API_PATH = `api/${APP_ID}`; export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`; export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`; +export const API_ROOT_PATH = `/${GIS_API_PATH}`; + +export const MVT_GETTILE_API_PATH = 'mvt/getTile'; +export const MVT_SOURCE_LAYER_NAME = 'source_layer'; +export const KBN_TOO_MANY_FEATURES_PROPERTY = '__kbn_too_many_features__'; +export const KBN_TOO_MANY_FEATURES_IMAGE_ID = '__kbn_too_many_features_image_id__'; const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`; export function getNewMapPath() { @@ -220,6 +226,7 @@ export enum SCALING_TYPES { LIMIT = 'LIMIT', CLUSTERS = 'CLUSTERS', TOP_HITS = 'TOP_HITS', + MVT = 'MVT', } export const RGBA_0000 = 'rgba(0,0,0,0)'; diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index cd7d2d5d0f461..f3521cca2e456 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -18,7 +18,6 @@ export type MapFilters = { refreshTimerLastTriggeredAt?: string; timeFilters: TimeRange; zoom: number; - geogridPrecision?: number; }; type ESSearchSourceSyncMeta = { diff --git a/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts b/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts index 44250360e9d00..e57efca94d95e 100644 --- a/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts +++ b/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FeatureCollection, GeoJsonProperties } from 'geojson'; import { MapExtent } from './descriptor_types'; +import { ES_GEO_FIELD_TYPE } from './constants'; export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent; @@ -13,3 +15,11 @@ export function turfBboxToBounds(turfBbox: unknown): MapExtent; export function clampToLatBounds(lat: number): number; export function clampToLonBounds(lon: number): number; + +export function hitsToGeoJson( + hits: Array>, + flattenHit: (elasticSearchHit: Record) => GeoJsonProperties, + geoFieldName: string, + geoFieldType: ES_GEO_FIELD_TYPE, + epochMillisFields: string[] +): FeatureCollection; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index f408896853155..b00594cb7fb23 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -271,7 +271,7 @@ export function triggerRefreshTimer() { }; } -export function updateDrawState(drawState: DrawState) { +export function updateDrawState(drawState: DrawState | null) { return (dispatch: Dispatch) => { if (drawState !== null) { dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way diff --git a/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts b/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts index 9faa33fae5a43..543dbf6d87039 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts @@ -15,18 +15,22 @@ import { IVectorSource } from '../sources/vector_source'; export class ESDocField extends AbstractField implements IField { private readonly _source: IESSource; + private readonly _canReadFromGeoJson: boolean; constructor({ fieldName, source, origin, + canReadFromGeoJson = true, }: { fieldName: string; source: IESSource; origin: FIELD_ORIGIN; + canReadFromGeoJson?: boolean; }) { super({ fieldName, origin }); this._source = source; + this._canReadFromGeoJson = canReadFromGeoJson; } canValueBeFormatted(): boolean { @@ -60,6 +64,10 @@ export class ESDocField extends AbstractField implements IField { return true; } + canReadFromGeoJson(): boolean { + return this._canReadFromGeoJson; + } + async getOrdinalFieldMetaRequest(): Promise { const indexPatternField = await this._getIndexPatternField(); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx index faae26cac08e7..822b78aa0deff 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx @@ -128,32 +128,41 @@ describe('syncData', () => { sinon.assert.notCalled(syncContext2.stopLoading); }); - it('Should resync when changes to source params', async () => { - const layer1: TiledVectorLayer = createLayer({}, {}); - const syncContext1 = new MockSyncContext({ dataFilters: {} }); - - await layer1.syncData(syncContext1); - - const dataRequestDescriptor: DataRequestDescriptor = { - data: defaultConfig, - dataId: 'source', - }; - const layer2: TiledVectorLayer = createLayer( - { - __dataRequests: [dataRequestDescriptor], - }, - { layerName: 'barfoo' } - ); - const syncContext2 = new MockSyncContext({ dataFilters: {} }); - await layer2.syncData(syncContext2); - - // @ts-expect-error - sinon.assert.calledOnce(syncContext2.startLoading); - // @ts-expect-error - sinon.assert.calledOnce(syncContext2.stopLoading); - - // @ts-expect-error - const call = syncContext2.stopLoading.getCall(0); - expect(call.args[2]).toEqual({ ...defaultConfig, layerName: 'barfoo' }); + describe('Should resync when changes to source params: ', () => { + [ + { layerName: 'barfoo' }, + { urlTemplate: 'https://sub.example.com/{z}/{x}/{y}.pbf' }, + { minSourceZoom: 1 }, + { maxSourceZoom: 12 }, + ].forEach((changes) => { + it(`change in ${Object.keys(changes).join(',')}`, async () => { + const layer1: TiledVectorLayer = createLayer({}, {}); + const syncContext1 = new MockSyncContext({ dataFilters: {} }); + + await layer1.syncData(syncContext1); + + const dataRequestDescriptor: DataRequestDescriptor = { + data: defaultConfig, + dataId: 'source', + }; + const layer2: TiledVectorLayer = createLayer( + { + __dataRequests: [dataRequestDescriptor], + }, + changes + ); + const syncContext2 = new MockSyncContext({ dataFilters: {} }); + await layer2.syncData(syncContext2); + + // @ts-expect-error + sinon.assert.calledOnce(syncContext2.startLoading); + // @ts-expect-error + sinon.assert.calledOnce(syncContext2.stopLoading); + + // @ts-expect-error + const call = syncContext2.stopLoading.getCall(0); + expect(call.args[2]).toEqual({ ...defaultConfig, ...changes }); + }); + }); }); }); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index c9ae1c805fa30..70bf8ea3883b7 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -63,21 +63,24 @@ export class TiledVectorLayer extends VectorLayer { ); const prevDataRequest = this.getSourceDataRequest(); + const templateWithMeta = await this._source.getUrlTemplateWithMeta(searchFilters); if (prevDataRequest) { const data: MVTSingleLayerVectorSourceConfig = prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig; - const canSkipBecauseNoChanges = - data.layerName === this._source.getLayerName() && - data.minSourceZoom === this._source.getMinZoom() && - data.maxSourceZoom === this._source.getMaxZoom(); - - if (canSkipBecauseNoChanges) { - return null; + if (data) { + const canSkipBecauseNoChanges = + data.layerName === this._source.getLayerName() && + data.minSourceZoom === this._source.getMinZoom() && + data.maxSourceZoom === this._source.getMaxZoom() && + data.urlTemplate === templateWithMeta.urlTemplate; + + if (canSkipBecauseNoChanges) { + return null; + } } } startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters); try { - const templateWithMeta = await this._source.getUrlTemplateWithMeta(); stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, templateWithMeta, {}); } catch (error) { onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); @@ -160,6 +163,11 @@ export class TiledVectorLayer extends VectorLayer { return false; } + if (!mbTileSource.tiles) { + // Expected source is not compatible, so remove. + return true; + } + const isSourceDifferent = mbTileSource.tiles[0] !== tiledSourceMeta.urlTemplate || mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom || diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 2ba7f750e9b40..c49d0044e6ad6 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -15,9 +15,11 @@ import { SOURCE_BOUNDS_DATA_REQUEST_ID, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, + KBN_TOO_MANY_FEATURES_PROPERTY, LAYER_TYPE, FIELD_ORIGIN, LAYER_STYLE_TYPE, + KBN_TOO_MANY_FEATURES_IMAGE_ID, } from '../../../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; @@ -777,6 +779,8 @@ export class VectorLayer extends AbstractLayer { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); + const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId(); + const hasJoins = this.hasJoins(); if (!mbMap.getLayer(fillLayerId)) { const mbLayer = { @@ -802,6 +806,30 @@ export class VectorLayer extends AbstractLayer { } mbMap.addLayer(mbLayer); } + if (!mbMap.getLayer(tooManyFeaturesLayerId)) { + const mbLayer = { + id: tooManyFeaturesLayerId, + type: 'fill', + source: sourceId, + paint: {}, + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); + mbMap.setFilter(tooManyFeaturesLayerId, [ + '==', + ['get', KBN_TOO_MANY_FEATURES_PROPERTY], + true, + ]); + mbMap.setPaintProperty( + tooManyFeaturesLayerId, + 'fill-pattern', + KBN_TOO_MANY_FEATURES_IMAGE_ID + ); + mbMap.setPaintProperty(tooManyFeaturesLayerId, 'fill-opacity', this.getAlpha()); + } + this.getCurrentStyle().setMBPaintProperties({ alpha: this.getAlpha(), mbMap, @@ -822,6 +850,9 @@ export class VectorLayer extends AbstractLayer { if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) { mbMap.setFilter(lineLayerId, lineFilterExpr); } + + this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId); + mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); } _syncStylePropertiesWithMb(mbMap) { @@ -836,6 +867,19 @@ export class VectorLayer extends AbstractLayer { type: 'geojson', data: EMPTY_FEATURE_COLLECTION, }); + } else if (mbSource.type !== 'geojson') { + // Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer. + this.getMbLayerIds().forEach((mbLayerId) => { + if (mbMap.getLayer(mbLayerId)) { + mbMap.removeLayer(mbLayerId); + } + }); + + mbMap.removeSource(this._getMbSourceId()); + mbMap.addSource(this._getMbSourceId(), { + type: 'geojson', + data: EMPTY_FEATURE_COLLECTION, + }); } } @@ -865,6 +909,10 @@ export class VectorLayer extends AbstractLayer { return this.makeMbLayerId('fill'); } + _getMbTooManyFeaturesLayerId() { + return this.makeMbLayerId('toomanyfeatures'); + } + getMbLayerIds() { return [ this._getMbPointLayerId(), @@ -872,6 +920,7 @@ export class VectorLayer extends AbstractLayer { this._getMbSymbolLayerId(), this._getMbLineLayerId(), this._getMbPolygonLayerId(), + this._getMbTooManyFeaturesLayerId(), ]; } diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx index 52524d0c9a5fa..c5d6ced76b5c0 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx @@ -6,7 +6,6 @@ import { EMSFileSource } from './ems_file_source'; -jest.mock('ui/new_platform'); jest.mock('../../layers/vector_layer/vector_layer', () => {}); function makeEMSFileSource(tooltipProperties: string[]) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts index 87abbedfdf50e..cf0170ab7f1bd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts @@ -11,8 +11,6 @@ import _ from 'lodash'; import { AGG_TYPE } from '../../../../common/constants'; import { AggDescriptor } from '../../../../common/descriptor_types'; -jest.mock('ui/new_platform'); - const sumFieldName = 'myFieldGettingSummed'; const metricExamples = [ { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 37193e148bdc7..2e0ba7cf3efee 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -3,10 +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 { MapExtent, MapFilters } from '../../../../common/descriptor_types'; +import { MapExtent, VectorSourceRequestMeta } from '../../../../common/descriptor_types'; jest.mock('../../../kibana_services'); -jest.mock('ui/new_platform'); import { getIndexPatternService, getSearchService } from '../../../kibana_services'; import { ESGeoGridSource } from './es_geo_grid_source'; @@ -20,6 +19,7 @@ import { SearchSource } from '../../../../../../../src/plugins/data/public/searc export class MockSearchSource { setField = jest.fn(); + setParent() {} } describe('ESGeoGridSource', () => { @@ -105,6 +105,9 @@ describe('ESGeoGridSource', () => { async create() { return mockSearchSource as SearchSource; }, + createEmpty() { + return mockSearchSource as SearchSource; + }, }, }; @@ -121,7 +124,7 @@ describe('ESGeoGridSource', () => { maxLat: 80, }; - const mapFilters: MapFilters = { + const mapFilters: VectorSourceRequestMeta = { geogridPrecision: 4, filters: [], timeFilters: { @@ -129,8 +132,16 @@ describe('ESGeoGridSource', () => { to: '15m', mode: 'relative', }, - // extent, + extent, + applyGlobalQuery: true, + fieldNames: [], buffer: extent, + sourceQuery: { + query: '', + language: 'KQL', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z', + }, + sourceMeta: null, zoom: 0, }; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap index 8ebb389472f74..dd62be11c679d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should disable clusters option when clustering is not supported 1`] = ` +exports[`scaling form should disable clusters option when clustering is not supported 1`] = ` + + + + Use vector tiles for faster display of large datasets. + + } + delay="regular" + position="left" + > + + +
    + + + + + +`; + +exports[`scaling form should disable mvt option when mvt is not supported 1`] = ` + + +
    + +
    +
    + + +
    + + + + + +
    `; -exports[`should render 1`] = ` +exports[`scaling form should render 1`] = ` + + + + Use vector tiles for faster display of large datasets. + + } + delay="regular" + position="left" + > + +
    `; -exports[`should render top hits form when scaling type is TOP_HITS 1`] = ` +exports[`scaling form should render top hits form when scaling type is TOP_HITS 1`] = ` + + + + Use vector tiles for faster display of large datasets. + + } + delay="regular" + position="left" + > + +
    @@ -159,6 +162,8 @@ export class CreateSourceEditor extends Component { this.state.indexPattern, this.state.geoFieldName )} + supportsMvt={mvtSupported} + mvtDisabledReason={mvtSupported ? null : getMvtDisabledReason()} clusteringDisabledReason={ this.state.indexPattern ? getGeoTileAggNotSupportedReason( diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index 1ec6d2a1ff671..249b9a2454d7d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -14,13 +14,18 @@ import { ESSearchSource, sourceTitle } from './es_search_source'; import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer'; import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants'; +import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) { const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig); - return sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS - ? BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors) - : VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + if (sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS) { + return BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + } else if (sourceDescriptor.scalingType === SCALING_TYPES.MVT) { + return TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + } else { + return VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + } } export const esDocumentsLayerWizardConfig: LayerWizard = { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts index 23e3c759d73c3..67d68dc065b00 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts @@ -5,11 +5,22 @@ */ import { AbstractESSource } from '../es_source'; -import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; +import { ESSearchSourceDescriptor, MapFilters } from '../../../../common/descriptor_types'; +import { ITiledSingleLayerVectorSource } from '../vector_source'; -export class ESSearchSource extends AbstractESSource { +export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource { static createDescriptor(sourceConfig: unknown): ESSearchSourceDescriptor; constructor(sourceDescriptor: Partial, inspectorAdapters: unknown); getFieldNames(): string[]; + + getUrlTemplateWithMeta( + searchFilters: MapFilters + ): Promise<{ + layerName: string; + urlTemplate: string; + minSourceZoom: number; + maxSourceZoom: number; + }>; + getLayerName(): string; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js index 6d61c4a7455b2..7ac2738eaeb51 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js @@ -6,9 +6,10 @@ import _ from 'lodash'; import React from 'react'; +import rison from 'rison-node'; import { AbstractESSource } from '../es_source'; -import { getSearchService } from '../../../kibana_services'; +import { getSearchService, getHttp } from '../../../kibana_services'; import { hitsToGeoJson } from '../../../../common/elasticsearch_geo_utils'; import { UpdateSourceEditor } from './update_source_editor'; import { @@ -18,6 +19,9 @@ import { SORT_ORDER, SCALING_TYPES, VECTOR_SHAPE_TYPE, + MVT_SOURCE_LAYER_NAME, + GIS_API_PATH, + MVT_GETTILE_API_PATH, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -96,6 +100,7 @@ export class ESSearchSource extends AbstractESSource { return new ESDocField({ fieldName, source: this, + canReadFromGeoJson: this._descriptor.scalingType !== SCALING_TYPES.MVT, }); } @@ -448,9 +453,13 @@ export class ESSearchSource extends AbstractESSource { } isFilterByMapBounds() { - return this._descriptor.scalingType === SCALING_TYPES.CLUSTER - ? true - : this._descriptor.filterByMapBounds; + if (this._descriptor.scalingType === SCALING_TYPES.CLUSTER) { + return true; + } else if (this._descriptor.scalingType === SCALING_TYPES.MVT) { + return false; + } else { + return this._descriptor.filterByMapBounds; + } } async getLeftJoinFields() { @@ -553,11 +562,65 @@ export class ESSearchSource extends AbstractESSource { } getJoinsDisabledReason() { - return this._descriptor.scalingType === SCALING_TYPES.CLUSTERS - ? i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', { - defaultMessage: 'Joins are not supported when scaling by clusters', - }) - : null; + let reason; + if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) { + reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', { + defaultMessage: 'Joins are not supported when scaling by clusters', + }); + } else if (this._descriptor.scalingType === SCALING_TYPES.MVT) { + reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReasonMvt', { + defaultMessage: 'Joins are not supported when scaling by mvt vector tiles', + }); + } else { + reason = null; + } + return reason; + } + + getLayerName() { + return MVT_SOURCE_LAYER_NAME; + } + + async getUrlTemplateWithMeta(searchFilters) { + const indexPattern = await this.getIndexPattern(); + const indexSettings = await loadIndexSettings(indexPattern.title); + + const { docValueFields, sourceOnlyFields } = getDocValueAndSourceFields( + indexPattern, + searchFilters.fieldNames + ); + + const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source + + const searchSource = await this.makeSearchSource( + searchFilters, + indexSettings.maxResultWindow, + initialSearchContext + ); + searchSource.setField('fields', searchFilters.fieldNames); // Setting "fields" filters out unused scripted fields + if (sourceOnlyFields.length === 0) { + searchSource.setField('source', false); // do not need anything from _source + } else { + searchSource.setField('source', sourceOnlyFields); + } + if (this._hasSort()) { + searchSource.setField('sort', this._buildEsSort()); + } + + const dsl = await searchSource.getSearchRequestBody(); + const risonDsl = rison.encode(dsl); + + const mvtUrlServicePath = getHttp().basePath.prepend( + `/${GIS_API_PATH}/${MVT_GETTILE_API_PATH}` + ); + + const urlTemplate = `${mvtUrlServicePath}?x={x}&y={y}&z={z}&geometryFieldName=${this._descriptor.geoField}&index=${indexPattern.title}&requestBody=${risonDsl}`; + return { + layerName: this.getLayerName(), + minSourceZoom: this.getMinZoom(), + maxSourceZoom: this.getMaxZoom(), + urlTemplate: urlTemplate, + }; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts new file mode 100644 index 0000000000000..3223d0c94178f --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts @@ -0,0 +1,155 @@ +/* + * 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 { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants'; + +jest.mock('../../../kibana_services'); +jest.mock('./load_index_settings'); + +import { getIndexPatternService, getSearchService, getHttp } from '../../../kibana_services'; +import { SearchSource } from '../../../../../../../src/plugins/data/public/search/search_source'; + +// @ts-expect-error +import { loadIndexSettings } from './load_index_settings'; + +import { ESSearchSource } from './es_search_source'; +import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; + +describe('ESSearchSource', () => { + it('constructor', () => { + const esSearchSource = new ESSearchSource({}, null); + expect(esSearchSource instanceof ESSearchSource).toBe(true); + }); + + describe('ITiledSingleLayerVectorSource', () => { + it('mb-source params', () => { + const esSearchSource = new ESSearchSource({}, null); + expect(esSearchSource.getMinZoom()).toBe(0); + expect(esSearchSource.getMaxZoom()).toBe(24); + expect(esSearchSource.getLayerName()).toBe('source_layer'); + }); + + describe('getUrlTemplateWithMeta', () => { + const geoFieldName = 'bar'; + const mockIndexPatternService = { + get() { + return { + title: 'foobar-title-*', + fields: { + getByName() { + return { + name: geoFieldName, + type: ES_GEO_FIELD_TYPE.GEO_SHAPE, + }; + }, + }, + }; + }, + }; + + beforeEach(async () => { + const mockSearchSource = { + setField: jest.fn(), + getSearchRequestBody() { + return { foobar: 'ES_DSL_PLACEHOLDER', params: this.setField.mock.calls }; + }, + setParent() {}, + }; + const mockSearchService = { + searchSource: { + async create() { + return (mockSearchSource as unknown) as SearchSource; + }, + createEmpty() { + return (mockSearchSource as unknown) as SearchSource; + }, + }, + }; + + // @ts-expect-error + getIndexPatternService.mockReturnValue(mockIndexPatternService); + // @ts-expect-error + getSearchService.mockReturnValue(mockSearchService); + loadIndexSettings.mockReturnValue({ + maxResultWindow: 1000, + }); + // @ts-expect-error + getHttp.mockReturnValue({ + basePath: { + prepend(path: string) { + return `rootdir${path};`; + }, + }, + }); + }); + + const searchFilters: VectorSourceRequestMeta = { + filters: [], + zoom: 0, + fieldNames: ['tooltipField', 'styleField'], + timeFilters: { + from: 'now', + to: '15m', + mode: 'relative', + }, + sourceQuery: { + query: 'tooltipField: foobar', + language: 'KQL', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z', + }, + sourceMeta: null, + applyGlobalQuery: true, + }; + + it('Should only include required props', async () => { + const esSearchSource = new ESSearchSource( + { geoField: geoFieldName, indexPatternId: 'ipId' }, + null + ); + const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta(searchFilters); + expect(urlTemplateWithMeta.urlTemplate).toBe( + `rootdir/api/maps/mvt/getTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':fields,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))` + ); + }); + }); + }); + + describe('isFilterByMapBounds', () => { + it('default', () => { + const esSearchSource = new ESSearchSource({}, null); + expect(esSearchSource.isFilterByMapBounds()).toBe(true); + }); + it('mvt', () => { + const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null); + expect(esSearchSource.isFilterByMapBounds()).toBe(false); + }); + }); + + describe('getJoinsDisabledReason', () => { + it('default', () => { + const esSearchSource = new ESSearchSource({}, null); + expect(esSearchSource.getJoinsDisabledReason()).toBe(null); + }); + it('mvt', () => { + const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null); + expect(esSearchSource.getJoinsDisabledReason()).toBe( + 'Joins are not supported when scaling by mvt vector tiles' + ); + }); + }); + + describe('getFields', () => { + it('default', () => { + const esSearchSource = new ESSearchSource({}, null); + const docField = esSearchSource.createField({ fieldName: 'prop1' }); + expect(docField.canReadFromGeoJson()).toBe(true); + }); + it('mvt', () => { + const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null); + const docField = esSearchSource.createField({ fieldName: 'prop1' }); + expect(docField.canReadFromGeoJson()).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx index 6e56c179b4ead..f57335db14c62 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx @@ -27,28 +27,46 @@ const defaultProps = { termFields: [], topHitsSplitField: null, topHitsSize: 1, + supportsMvt: true, + mvtDisabledReason: null, }; -test('should render', async () => { - const component = shallow(); +describe('scaling form', () => { + test('should render', async () => { + const component = shallow(); - expect(component).toMatchSnapshot(); -}); + expect(component).toMatchSnapshot(); + }); -test('should disable clusters option when clustering is not supported', async () => { - const component = shallow( - - ); + test('should disable clusters option when clustering is not supported', async () => { + const component = shallow( + + ); - expect(component).toMatchSnapshot(); -}); + expect(component).toMatchSnapshot(); + }); + + test('should render top hits form when scaling type is TOP_HITS', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); -test('should render top hits form when scaling type is TOP_HITS', async () => { - const component = shallow(); + test('should disable mvt option when mvt is not supported', async () => { + const component = shallow( + + ); - expect(component).toMatchSnapshot(); + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx index 816db6a98d593..cc2d4d059a3a8 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx @@ -4,16 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { EuiFormRow, + EuiHorizontalRule, + EuiRadio, + EuiSpacer, EuiSwitch, EuiSwitchEvent, EuiTitle, - EuiSpacer, - EuiHorizontalRule, - EuiRadio, EuiToolTip, + EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -24,8 +25,8 @@ import { ValidatedRange } from '../../../components/validated_range'; import { DEFAULT_MAX_INNER_RESULT_WINDOW, DEFAULT_MAX_RESULT_WINDOW, - SCALING_TYPES, LAYER_TYPE, + SCALING_TYPES, } from '../../../../common/constants'; // @ts-ignore import { loadIndexSettings } from './load_index_settings'; @@ -38,7 +39,9 @@ interface Props { onChange: (args: OnSourceChangeArgs) => void; scalingType: SCALING_TYPES; supportsClustering: boolean; + supportsMvt: boolean; clusteringDisabledReason?: string | null; + mvtDisabledReason?: string | null; termFields: IFieldType[]; topHitsSplitField: string | null; topHitsSize: number; @@ -80,8 +83,15 @@ export class ScalingForm extends Component { } _onScalingTypeChange = (optionId: string): void => { - const layerType = - optionId === SCALING_TYPES.CLUSTERS ? LAYER_TYPE.BLENDED_VECTOR : LAYER_TYPE.VECTOR; + let layerType; + if (optionId === SCALING_TYPES.CLUSTERS) { + layerType = LAYER_TYPE.BLENDED_VECTOR; + } else if (optionId === SCALING_TYPES.MVT) { + layerType = LAYER_TYPE.TILED_VECTOR; + } else { + layerType = LAYER_TYPE.VECTOR; + } + this.props.onChange({ propName: 'scalingType', value: optionId, newLayerType: layerType }); }; @@ -177,9 +187,47 @@ export class ScalingForm extends Component { ); } + _renderMVTRadio() { + const labelText = i18n.translate('xpack.maps.source.esSearch.useMVTVectorTiles', { + defaultMessage: 'Use vector tiles', + }); + const mvtRadio = ( + this._onScalingTypeChange(SCALING_TYPES.MVT)} + disabled={!this.props.supportsMvt} + /> + ); + + const enabledInfo = ( + <> + + + {i18n.translate('xpack.maps.source.esSearch.mvtDescription', { + defaultMessage: 'Use vector tiles for faster display of large datasets.', + })} + + ); + + return !this.props.supportsMvt ? ( + + {mvtRadio} + + ) : ( + + {mvtRadio} + + ); + } + render() { let filterByBoundsSwitch; - if (this.props.scalingType !== SCALING_TYPES.CLUSTERS) { + if ( + this.props.scalingType === SCALING_TYPES.TOP_HITS || + this.props.scalingType === SCALING_TYPES.LIMIT + ) { filterByBoundsSwitch = ( { ); } - let scalingForm = null; + let topHitsOptionsForm = null; if (this.props.scalingType === SCALING_TYPES.TOP_HITS) { - scalingForm = ( + topHitsOptionsForm = ( {this._renderTopHitsForm()} @@ -234,12 +282,12 @@ export class ScalingForm extends Component { onChange={() => this._onScalingTypeChange(SCALING_TYPES.TOP_HITS)} /> {this._renderClusteringRadio()} + {this._renderMVTRadio()}
  • {filterByBoundsSwitch} - - {scalingForm} + {topHitsOptionsForm} ); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js index 0701dbbaecdd5..c123c307c4895 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js @@ -17,6 +17,8 @@ import { getTermsFields, getSourceFields, supportsGeoTileAgg, + supportsMvt, + getMvtDisabledReason, } from '../../../index_pattern_util'; import { SORT_ORDER } from '../../../../common/constants'; import { ESDocField } from '../../fields/es_doc_field'; @@ -42,6 +44,9 @@ export class UpdateSourceEditor extends Component { termFields: null, sortFields: null, supportsClustering: false, + supportsMvt: false, + mvtDisabledReason: null, + clusteringDisabledReason: null, }; componentDidMount() { @@ -94,9 +99,12 @@ export class UpdateSourceEditor extends Component { }); }); + const mvtSupported = supportsMvt(indexPattern, geoField.name); this.setState({ supportsClustering: supportsGeoTileAgg(geoField), + supportsMvt: mvtSupported, clusteringDisabledReason: getGeoTileAggNotSupportedReason(geoField), + mvtDisabledReason: mvtSupported ? null : getMvtDisabledReason(), sourceFields: sourceFields, termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields sortFields: indexPattern.fields.filter( @@ -207,7 +215,9 @@ export class UpdateSourceEditor extends Component { onChange={this.props.onChange} scalingType={this.props.scalingType} supportsClustering={this.state.supportsClustering} + supportsMvt={this.state.supportsMvt} clusteringDisabledReason={this.state.clusteringDisabledReason} + mvtDisabledReason={this.state.mvtDisabledReason} termFields={this.state.termFields} topHitsSplitField={this.props.topHitsSplitField} topHitsSize={this.props.topHitsSize} 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 8cc2aa018979b..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 @@ -184,7 +184,7 @@ export class AbstractESSource extends AbstractVectorSource { const minLon = esBounds.top_left.lon; const maxLon = esBounds.bottom_right.lon; return { - minLon: minLon > maxLon ? minLon - 360 : minLon, + minLon: minLon > maxLon ? minLon - 360 : minLon, //fixes an ES bbox to straddle dateline maxLon, minLat: esBounds.bottom_right.lat, maxLat: esBounds.top_left.lat, @@ -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/sources/es_term_source/es_term_source.test.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js index f6779206868a5..060096157f578 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js @@ -6,7 +6,6 @@ import { ESTermSource, extractPropertiesMap } from './es_term_source'; -jest.mock('ui/new_platform'); jest.mock('../../layers/vector_layer/vector_layer', () => {}); const indexPatternTitle = 'myIndex'; diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts index 271505010f36a..fd9c179275444 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts @@ -14,6 +14,7 @@ import { MapExtent, MapFilters, MapQuery, + VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; @@ -64,7 +65,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc ): MapExtent | null; getGeoJsonWithMeta( layerName: string, - searchFilters: MapFilters, + searchFilters: VectorSourceRequestMeta, registerCancelCallback: (callback: () => void) => void ): Promise; @@ -79,7 +80,9 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc } export interface ITiledSingleLayerVectorSource extends IVectorSource { - getUrlTemplateWithMeta(): Promise<{ + getUrlTemplateWithMeta( + searchFilters: VectorSourceRequestMeta + ): Promise<{ layerName: string; urlTemplate: string; minSourceZoom: number; diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx index aa4dbc67e8e4d..f082e67512099 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx @@ -9,8 +9,6 @@ import { shallow } from 'enzyme'; import { HeatmapStyleEditor } from './heatmap_style_editor'; -jest.mock('ui/new_platform'); - describe('HeatmapStyleEditor', () => { test('is rendered', () => { const component = shallow( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx index e49c15c68b8db..2a544b94d760a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx @@ -14,6 +14,7 @@ import { FieldMetaOptions } from '../../../../../../common/descriptor_types'; type Props = { fieldMetaOptions: FieldMetaOptions; onChange: (fieldMetaOptions: FieldMetaOptions) => void; + switchDisabled: boolean; }; export function CategoricalFieldMetaPopover(props: Props) { @@ -34,6 +35,7 @@ export function CategoricalFieldMetaPopover(props: Props) { checked={props.fieldMetaOptions.isEnabled} onChange={onIsEnabledChange} compressed + disabled={props.switchDisabled} /> diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx index 9086c4df31596..09be9d72af970 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx @@ -40,6 +40,7 @@ type Props = { fieldMetaOptions: FieldMetaOptions; styleName: VECTOR_STYLES; onChange: (fieldMetaOptions: FieldMetaOptions) => void; + switchDisabled: boolean; }; export function OrdinalFieldMetaPopover(props: Props) { @@ -66,6 +67,7 @@ export function OrdinalFieldMetaPopover(props: Props) { checked={props.fieldMetaOptions.isEnabled} onChange={onIsEnabledChange} compressed + disabled={props.switchDisabled} /> diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.tsx.snap index c722e86512e52..34d2d7fb0cbbf 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.tsx.snap @@ -325,3 +325,29 @@ exports[`ordinal Should render ordinal legend as bands 1`] = ` `; + +exports[`renderFieldMetaPopover Should disable toggle when field is not backed by geojson source 1`] = ` + +`; + +exports[`renderFieldMetaPopover Should enable toggle when field is backed by geojson-source 1`] = ` + +`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx index 62a6a59dd091b..de8f3b5c09175 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { return
    mockVectorStyleEditor
    ; @@ -578,3 +577,39 @@ test('Should read out ordinal type correctly', async () => { expect(ordinalColorStyle2.isOrdinal()).toEqual(true); expect(ordinalColorStyle2.isCategorical()).toEqual(false); }); + +describe('renderFieldMetaPopover', () => { + test('Should enable toggle when field is backed by geojson-source', () => { + const colorStyle = makeProperty( + { + color: 'Blues', + type: undefined, + fieldMetaOptions, + }, + undefined, + mockField + ); + + const legendRow = colorStyle.renderFieldMetaPopover(() => {}); + expect(legendRow).toMatchSnapshot(); + }); + + test('Should disable toggle when field is not backed by geojson source', () => { + const nonGeoJsonField = Object.create(mockField); + nonGeoJsonField.canReadFromGeoJson = () => { + return false; + }; + const colorStyle = makeProperty( + { + color: 'Blues', + type: undefined, + fieldMetaOptions, + }, + undefined, + nonGeoJsonField + ); + + const legendRow = colorStyle.renderFieldMetaPopover(() => {}); + expect(legendRow).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index af93c8e0c9d6d..06987ab8bcc48 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -6,7 +6,6 @@ import { shallow } from 'enzyme'; -jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { return
    mockVectorStyleEditor
    ; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx index db44ae0da562d..c5298067f6cbe 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { return
    mockVectorStyleEditor
    ; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index b16755e69f92d..f6ab052497723 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -328,16 +328,20 @@ export class DynamicStyleProperty return null; } + const switchDisabled = !!this._field && !this._field.canReadFromGeoJson(); + return this.isCategorical() ? ( ) : ( ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js index a85cd0cc86407..28801a402ca14 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js @@ -14,7 +14,6 @@ import { } from '../../../../common/constants'; jest.mock('../../../kibana_services'); -jest.mock('ui/new_platform'); class MockField { constructor({ fieldName }) { 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/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index 8da6fa2318de9..0da6f632eb4a8 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -4,32 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; +import { + GEO_JSON_TYPE, + FEATURE_VISIBLE_PROPERTY_NAME, + KBN_TOO_MANY_FEATURES_PROPERTY, +} from '../../../common/constants'; + +export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true]; const VISIBILITY_FILTER_CLAUSE = ['all', ['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]]; +const TOO_MANY_FEATURES_FILTER = ['all', EXCLUDE_TOO_MANY_FEATURES_BOX]; const CLOSED_SHAPE_MB_FILTER = [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ...TOO_MANY_FEATURES_FILTER, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ], ]; const VISIBLE_CLOSED_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, CLOSED_SHAPE_MB_FILTER]; const ALL_SHAPE_MB_FILTER = [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + ...TOO_MANY_FEATURES_FILTER, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + ], ]; const VISIBLE_ALL_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, ALL_SHAPE_MB_FILTER]; const POINT_MB_FILTER = [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + ...TOO_MANY_FEATURES_FILTER, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + ], ]; const VISIBLE_POINT_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, POINT_MB_FILTER]; diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx b/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx index cbeb4e79a38d7..7e18088444129 100644 --- a/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx +++ b/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx @@ -27,6 +27,8 @@ export type FieldProps = { name: string; }; +type FieldOptions = Array>; + function sortByLabel(a: EuiSelectableOption, b: EuiSelectableOption): number { return a.label.localeCompare(b.label); } @@ -66,7 +68,7 @@ interface Props { interface State { isPopoverOpen: boolean; checkedFields: string[]; - options?: EuiSelectableOption[]; + options?: FieldOptions; prevFields?: FieldProps[]; prevSelectedFields?: FieldProps[]; } @@ -105,13 +107,15 @@ export class AddTooltipFieldPopover extends Component { }); }; - _onSelect = (options: EuiSelectableOption[]) => { + _onSelect = (selectableOptions: EuiSelectableOption[]) => { + // EUI team to remove casting: https://github.com/elastic/eui/issues/3966 + const options = selectableOptions as FieldOptions; const checkedFields: string[] = options .filter((option) => { return option.checked === 'on'; }) .map((option) => { - return option.value as string; + return option.value!; }); this.setState({ diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap index b005e3ca6b17d..99cad61d6b2b4 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap @@ -57,6 +57,8 @@ exports[`LayerPanel is rendered 1`] = ` buttonContent="Source details" id="accordion1" initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} paddingSize="none" > { + mbMap.addImage(KBN_TOO_MANY_FEATURES_IMAGE_ID, tooManyFeaturesImage); + }; + tooManyFeaturesImage.src = tooManyFeaturesImageSrc; + let emptyImage; mbMap.on('styleimagemissing', (e) => { if (emptyImage) { diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap deleted file mode 100644 index d7fa099fe9dbe..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap +++ /dev/null @@ -1,242 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Should render cancel button when drawing 1`] = ` - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="contextMenu" - isOpen={false} - ownFocus={false} - panelPaddingSize="none" - withTitle={true} - > - , - "id": 1, - "title": "Draw shape", - }, - Object { - "content": , - "id": 2, - "title": "Draw bounds", - }, - Object { - "content": , - "id": 3, - "title": "Draw distance", - }, - ] - } - /> - - - - - - - - -`; - -exports[`renders 1`] = ` - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="contextMenu" - isOpen={false} - ownFocus={false} - panelPaddingSize="none" - withTitle={true} -> - , - "id": 1, - "title": "Draw shape", - }, - Object { - "content": , - "id": 2, - "title": "Draw bounds", - }, - Object { - "content": , - "id": 3, - "title": "Draw distance", - }, - ] - } - /> - -`; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap new file mode 100644 index 0000000000000..afeee04358e15 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap @@ -0,0 +1,242 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should render cancel button when drawing 1`] = ` + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="toolsControlPopover" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} + > + , + "id": 1, + "title": "Draw shape", + }, + Object { + "content": , + "id": 2, + "title": "Draw bounds", + }, + Object { + "content": , + "id": 3, + "title": "Draw distance", + }, + ] + } + /> + + + + + + + + +`; + +exports[`renders 1`] = ` + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="toolsControlPopover" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} +> + , + "id": 1, + "title": "Draw shape", + }, + Object { + "content": , + "id": 2, + "title": "Draw bounds", + }, + Object { + "content": , + "id": 3, + "title": "Draw distance", + }, + ] + } + /> + +`; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js deleted file mode 100644 index 5812604a91b87..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js +++ /dev/null @@ -1,30 +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 { ToolsControl } from './tools_control'; -import { isDrawingFilter } from '../../../selectors/map_selectors'; -import { updateDrawState } from '../../../actions'; - -function mapStateToProps(state = {}) { - return { - isDrawingFilter: isDrawingFilter(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - initiateDraw: (options) => { - dispatch(updateDrawState(options)); - }, - cancelDraw: () => { - dispatch(updateDrawState(null)); - }, - }; -} - -const connectedToolsControl = connect(mapStateToProps, mapDispatchToProps)(ToolsControl); -export { connectedToolsControl as ToolsControl }; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts new file mode 100644 index 0000000000000..70101f1ce6eba --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AnyAction, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { ToolsControl } from './tools_control'; +import { isDrawingFilter } from '../../../selectors/map_selectors'; +import { updateDrawState } from '../../../actions'; +import { MapStoreState } from '../../../reducers/store'; +import { DrawState } from '../../../../common/descriptor_types'; + +function mapStateToProps(state: MapStoreState) { + return { + isDrawingFilter: isDrawingFilter(state), + }; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + initiateDraw: (drawState: DrawState) => { + dispatch(updateDrawState(drawState)); + }, + cancelDraw: () => { + dispatch(updateDrawState(null)); + }, + }; +} + +const connectedToolsControl = connect(mapStateToProps, mapDispatchToProps)(ToolsControl); +export { connectedToolsControl as ToolsControl }; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js deleted file mode 100644 index 017f0369e0b73..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js +++ /dev/null @@ -1,227 +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 } from 'react'; -import { - EuiButtonIcon, - EuiPopover, - EuiContextMenu, - EuiFlexGroup, - EuiFlexItem, - EuiButton, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { DRAW_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { GeometryFilterForm } from '../../../components/geometry_filter_form'; -import { DistanceFilterForm } from '../../../components/distance_filter_form'; - -const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { - defaultMessage: 'Draw shape to filter data', -}); - -const DRAW_BOUNDS_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabel', { - defaultMessage: 'Draw bounds to filter data', -}); - -const DRAW_DISTANCE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawDistanceLabel', { - defaultMessage: 'Draw distance to filter data', -}); - -const DRAW_SHAPE_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabelShort', { - defaultMessage: 'Draw shape', -}); - -const DRAW_BOUNDS_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabelShort', { - defaultMessage: 'Draw bounds', -}); - -const DRAW_DISTANCE_LABEL_SHORT = i18n.translate( - 'xpack.maps.toolbarOverlay.drawDistanceLabelShort', - { - defaultMessage: 'Draw distance', - } -); - -export class ToolsControl extends Component { - state = { - isPopoverOpen: false, - }; - - _togglePopover = () => { - this.setState((prevState) => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); - }; - - _closePopover = () => { - this.setState({ isPopoverOpen: false }); - }; - - _initiateShapeDraw = (options) => { - this.props.initiateDraw({ - drawType: DRAW_TYPE.POLYGON, - ...options, - }); - this._closePopover(); - }; - - _initiateBoundsDraw = (options) => { - this.props.initiateDraw({ - drawType: DRAW_TYPE.BOUNDS, - ...options, - }); - this._closePopover(); - }; - - _initiateDistanceDraw = (options) => { - this.props.initiateDraw({ - drawType: DRAW_TYPE.DISTANCE, - ...options, - }); - this._closePopover(); - }; - - _getDrawPanels() { - const tools = [ - { - name: DRAW_SHAPE_LABEL, - panel: 1, - }, - { - name: DRAW_BOUNDS_LABEL, - panel: 2, - }, - ]; - - const hasGeoPoints = this.props.geoFields.some(({ geoFieldType }) => { - return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; - }); - if (hasGeoPoints) { - tools.push({ - name: DRAW_DISTANCE_LABEL, - panel: 3, - }); - } - - return [ - { - id: 0, - title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', { - defaultMessage: 'Tools', - }), - items: tools, - }, - { - id: 1, - title: DRAW_SHAPE_LABEL_SHORT, - content: ( - - ), - }, - { - id: 2, - title: DRAW_BOUNDS_LABEL_SHORT, - content: ( - - ), - }, - { - id: 3, - title: DRAW_DISTANCE_LABEL_SHORT, - content: ( - { - return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; - })} - getFilterActions={this.props.getFilterActions} - getActionContext={this.props.getActionContext} - onSubmit={this._initiateDistanceDraw} - /> - ), - }, - ]; - } - - _renderToolsButton() { - return ( - - ); - } - - render() { - const toolsPopoverButton = ( - - - - ); - - if (!this.props.isDrawingFilter) { - return toolsPopoverButton; - } - - return ( - - {toolsPopoverButton} - - - - - - - ); - } -} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js deleted file mode 100644 index f0e6dd43e68a1..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js +++ /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 React from 'react'; -import { shallow } from 'enzyme'; -import { ToolsControl } from './tools_control'; - -const defaultProps = { - initiateDraw: () => {}, - cancelDraw: () => {}, - geoFields: [ - { - geoFieldName: 'location', - geoFieldType: 'geo_point', - indexPatternTitle: 'my_index', - indexPatternId: '1', - }, - ], -}; - -test('renders', async () => { - const component = shallow(); - - expect(component).toMatchSnapshot(); -}); - -test('Should render cancel button when drawing', async () => { - const component = shallow(); - - expect(component).toMatchSnapshot(); -}); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.tsx new file mode 100644 index 0000000000000..544d789468e89 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ToolsControl } from './tools_control'; + +const defaultProps = { + initiateDraw: () => {}, + cancelDraw: () => {}, + geoFields: [ + { + geoFieldName: 'location', + geoFieldType: 'geo_point', + indexPatternTitle: 'my_index', + indexPatternId: '1', + }, + ], + isDrawingFilter: false, +}; + +test('renders', async () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); + +test('Should render cancel button when drawing', async () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx new file mode 100644 index 0000000000000..fa0864ce680a2 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx @@ -0,0 +1,263 @@ +/* + * 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 } from 'react'; +import { + EuiButtonIcon, + EuiPopover, + EuiContextMenu, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; +import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../common/constants'; +// @ts-expect-error +import { GeometryFilterForm } from '../../../components/geometry_filter_form'; +import { DistanceFilterForm } from '../../../components/distance_filter_form'; +import { GeoFieldWithIndex } from '../../../components/geo_field_with_index'; +import { DrawState } from '../../../../common/descriptor_types'; + +const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { + defaultMessage: 'Draw shape to filter data', +}); + +const DRAW_BOUNDS_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabel', { + defaultMessage: 'Draw bounds to filter data', +}); + +const DRAW_DISTANCE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawDistanceLabel', { + defaultMessage: 'Draw distance to filter data', +}); + +const DRAW_SHAPE_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabelShort', { + defaultMessage: 'Draw shape', +}); + +const DRAW_BOUNDS_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabelShort', { + defaultMessage: 'Draw bounds', +}); + +const DRAW_DISTANCE_LABEL_SHORT = i18n.translate( + 'xpack.maps.toolbarOverlay.drawDistanceLabelShort', + { + defaultMessage: 'Draw distance', + } +); + +interface Props { + cancelDraw: () => void; + geoFields: GeoFieldWithIndex[]; + initiateDraw: (drawState: DrawState) => void; + isDrawingFilter: boolean; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; +} + +interface State { + isPopoverOpen: boolean; +} + +export class ToolsControl extends Component { + state: State = { + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + _closePopover = () => { + this.setState({ isPopoverOpen: false }); + }; + + _initiateShapeDraw = (options: { + actionId: string; + geometryLabel: string; + indexPatternId: string; + geoFieldName: string; + geoFieldType: ES_GEO_FIELD_TYPE; + relation: ES_SPATIAL_RELATIONS; + }) => { + this.props.initiateDraw({ + drawType: DRAW_TYPE.POLYGON, + ...options, + }); + this._closePopover(); + }; + + _initiateBoundsDraw = (options: { + actionId: string; + geometryLabel: string; + indexPatternId: string; + geoFieldName: string; + geoFieldType: ES_GEO_FIELD_TYPE; + relation: ES_SPATIAL_RELATIONS; + }) => { + this.props.initiateDraw({ + drawType: DRAW_TYPE.BOUNDS, + ...options, + }); + this._closePopover(); + }; + + _initiateDistanceDraw = (options: { + actionId: string; + filterLabel: string; + indexPatternId: string; + geoFieldName: string; + }) => { + this.props.initiateDraw({ + drawType: DRAW_TYPE.DISTANCE, + ...options, + }); + this._closePopover(); + }; + + _getDrawPanels() { + const tools = [ + { + name: DRAW_SHAPE_LABEL, + panel: 1, + }, + { + name: DRAW_BOUNDS_LABEL, + panel: 2, + }, + ]; + + const hasGeoPoints = this.props.geoFields.some(({ geoFieldType }) => { + return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; + }); + if (hasGeoPoints) { + tools.push({ + name: DRAW_DISTANCE_LABEL, + panel: 3, + }); + } + + return [ + { + id: 0, + title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', { + defaultMessage: 'Tools', + }), + items: tools, + }, + { + id: 1, + title: DRAW_SHAPE_LABEL_SHORT, + content: ( + + ), + }, + { + id: 2, + title: DRAW_BOUNDS_LABEL_SHORT, + content: ( + + ), + }, + { + id: 3, + title: DRAW_DISTANCE_LABEL_SHORT, + content: ( + { + return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; + })} + getFilterActions={this.props.getFilterActions} + getActionContext={this.props.getActionContext} + onSubmit={this._initiateDistanceDraw} + /> + ), + }, + ]; + } + + _renderToolsButton() { + return ( + + ); + } + + render() { + const toolsPopoverButton = ( + + + + ); + + if (!this.props.isDrawingFilter) { + return toolsPopoverButton; + } + + return ( + + {toolsPopoverButton} + + + + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap index 8a5b7cf9186a8..0f620bdeb1c0e 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -53,7 +53,7 @@ exports[`TOCEntryActionsPopover is rendered 1`] = ` closePopover={[Function]} display="inlineBlock" hasArrow={true} - id="contextMenu" + id="testLayer" isOpen={false} ownFocus={false} panelPaddingSize="none" @@ -181,7 +181,7 @@ exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBou closePopover={[Function]} display="inlineBlock" hasArrow={true} - id="contextMenu" + id="testLayer" isOpen={false} ownFocus={false} panelPaddingSize="none" @@ -309,7 +309,7 @@ exports[`TOCEntryActionsPopover should have "show layer" action when layer is no closePopover={[Function]} display="inlineBlock" hasArrow={true} - id="contextMenu" + id="testLayer" isOpen={false} ownFocus={false} panelPaddingSize="none" @@ -437,7 +437,7 @@ exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1 closePopover={[Function]} display="inlineBlock" hasArrow={true} - id="contextMenu" + id="testLayer" isOpen={false} ownFocus={false} panelPaddingSize="none" diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index a1b9026fc57da..8e021461880ed 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -220,7 +220,7 @@ export class TOCEntryActionsPopover extends Component { render() { return ( { diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 239a2898a06fc..c1dfb61e9f3b6 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { MapsLegacyConfigType } from '../../../../src/plugins/maps_legacy/public'; +import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { MapsConfigType } from '../config'; import { MapsPluginStartDependencies } from './plugin'; import { CoreStart } from '../../../../src/core/public'; @@ -64,9 +64,8 @@ export const getShowMapsInspectorAdapter = () => getMapAppConfig().showMapsInspe export const getPreserveDrawingBuffer = () => getMapAppConfig().preserveDrawingBuffer; // map.* kibana.yml settings from maps_legacy plugin that are shared between OSS map visualizations and maps app -let kibanaCommonConfig: MapsLegacyConfigType; -export const setKibanaCommonConfig = (config: MapsLegacyConfigType) => - (kibanaCommonConfig = config); +let kibanaCommonConfig: MapsLegacyConfig; +export const setKibanaCommonConfig = (config: MapsLegacyConfig) => (kibanaCommonConfig = config); export const getKibanaCommonConfig = () => kibanaCommonConfig; export const getIsEmsEnabled = () => getKibanaCommonConfig().includeElasticMapsService; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index e03a085e9bc88..b08135b4e486c 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -44,7 +44,7 @@ import { MapsStartApi } from './api'; import { createSecurityLayerDescriptors, registerLayerWizard, registerSource } from './api'; import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; -import { MapsLegacyConfigType } from '../../../../src/plugins/maps_legacy/public'; +import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginStart } from '../../licensing/public'; import { StartContract as FileUploadStartContract } from '../../file_upload/public'; @@ -54,7 +54,7 @@ export interface MapsPluginSetupDependencies { home?: HomePublicPluginSetup; visualizations: VisualizationsSetup; embeddable: EmbeddableSetup; - mapsLegacy: { config: MapsLegacyConfigType }; + mapsLegacy: { config: MapsLegacyConfig }; share: SharePluginSetup; } 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/maps/server/mvt/__tests__/json/0_0_0_search.json b/x-pack/plugins/maps/server/mvt/__tests__/json/0_0_0_search.json new file mode 100644 index 0000000000000..0fc99ffd811f7 --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/__tests__/json/0_0_0_search.json @@ -0,0 +1 @@ +{"took":0,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":1,"relation":"eq"},"max_score":0,"hits":[{"_index":"poly","_id":"G7PRMXQBgyyZ-h5iYibj","_score":0,"_source":{"coordinates":{"coordinates":[[[-106.171875,36.59788913307022],[-50.625,-22.91792293614603],[4.921875,42.8115217450979],[-33.046875,63.54855223203644],[-66.796875,63.860035895395306],[-106.171875,36.59788913307022]]],"type":"polygon"}}}]}} diff --git a/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf new file mode 100644 index 0000000000000..9a9296e2ece3f Binary files /dev/null and b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf differ diff --git a/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts b/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts new file mode 100644 index 0000000000000..317d6434cf81e --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.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 * as path from 'path'; +import * as fs from 'fs'; + +const search000path = path.resolve(__dirname, './json/0_0_0_search.json'); +const search000raw = fs.readFileSync(search000path); +const search000json = JSON.parse((search000raw as unknown) as string); + +export const TILE_SEARCHES = { + '0.0.0': { + countResponse: { + count: 1, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + }, + searchResponse: search000json, + }, + '1.1.0': {}, +}; diff --git a/x-pack/plugins/maps/server/mvt/get_tile.test.ts b/x-pack/plugins/maps/server/mvt/get_tile.test.ts new file mode 100644 index 0000000000000..b9c928d594539 --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/get_tile.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { getTile } from './get_tile'; +import { TILE_SEARCHES } from './__tests__/tile_searches'; +import { Logger } from 'src/core/server'; +import * as path from 'path'; +import * as fs from 'fs'; + +describe('getTile', () => { + const mockCallElasticsearch = jest.fn(); + + const requestBody = { + _source: { excludes: [] }, + docvalue_fields: [], + query: { bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } }, + script_fields: {}, + size: 10000, + stored_fields: ['*'], + }; + const geometryFieldName = 'coordinates'; + + beforeEach(() => { + mockCallElasticsearch.mockReset(); + }); + + test('0.0.0 - under limit', async () => { + mockCallElasticsearch.mockImplementation((type) => { + if (type === 'count') { + return TILE_SEARCHES['0.0.0'].countResponse; + } else if (type === 'search') { + return TILE_SEARCHES['0.0.0'].searchResponse; + } else { + throw new Error(`${type} not recognized`); + } + }); + + const tile = await getTile({ + x: 0, + y: 0, + z: 0, + index: 'world_countries', + requestBody, + geometryFieldName, + logger: ({ + info: () => {}, + } as unknown) as Logger, + callElasticsearch: mockCallElasticsearch, + }); + + if (tile === null) { + throw new Error('Tile should be created'); + } + + const expectedPath = path.resolve(__dirname, './__tests__/pbf/0_0_0.pbf'); + const expectedBin = fs.readFileSync(expectedPath, 'binary'); + const expectedTile = Buffer.from(expectedBin, 'binary'); + expect(expectedTile.equals(tile)).toBe(true); + }); +}); diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts new file mode 100644 index 0000000000000..9621f7f174a30 --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/get_tile.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. + */ + +// @ts-expect-error +import geojsonvt from 'geojson-vt'; +// @ts-expect-error +import vtpbf from 'vt-pbf'; +import { Logger } from 'src/core/server'; +import { Feature, FeatureCollection, Polygon } from 'geojson'; +import { + ES_GEO_FIELD_TYPE, + FEATURE_ID_PROPERTY_NAME, + KBN_TOO_MANY_FEATURES_PROPERTY, + MVT_SOURCE_LAYER_NAME, +} from '../../common/constants'; + +import { hitsToGeoJson } from '../../common/elasticsearch_geo_utils'; +import { flattenHit } from './util'; + +interface ESBounds { + top_left: { + lon: number; + lat: number; + }; + bottom_right: { + lon: number; + lat: number; + }; +} + +export async function getTile({ + logger, + callElasticsearch, + index, + geometryFieldName, + x, + y, + z, + requestBody = {}, +}: { + x: number; + y: number; + z: number; + geometryFieldName: string; + index: string; + callElasticsearch: (type: string, ...args: any[]) => Promise; + logger: Logger; + requestBody: any; +}): Promise { + const geojsonBbox = tileToGeoJsonPolygon(x, y, z); + + let resultFeatures: Feature[]; + try { + let result; + try { + const geoShapeFilter = { + geo_shape: { + [geometryFieldName]: { + shape: geojsonBbox, + relation: 'INTERSECTS', + }, + }, + }; + requestBody.query.bool.filter.push(geoShapeFilter); + + const esSearchQuery = { + index, + body: requestBody, + }; + + const esCountQuery = { + index, + body: { + query: requestBody.query, + }, + }; + + const countResult = await callElasticsearch('count', esCountQuery); + + // @ts-expect-error + if (countResult.count > requestBody.size) { + // Generate "too many features"-bounds + const bboxAggName = 'data_bounds'; + const bboxQuery = { + index, + body: { + size: 0, + query: requestBody.query, + aggs: { + [bboxAggName]: { + geo_bounds: { + field: geometryFieldName, + }, + }, + }, + }, + }; + + const bboxResult = await callElasticsearch('search', bboxQuery); + + // @ts-expect-error + const bboxForData = esBboxToGeoJsonPolygon(bboxResult.aggregations[bboxAggName].bounds); + + resultFeatures = [ + { + type: 'Feature', + properties: { + [KBN_TOO_MANY_FEATURES_PROPERTY]: true, + }, + geometry: bboxForData, + }, + ]; + } else { + // Perform actual search + result = await callElasticsearch('search', esSearchQuery); + + // Todo: pass in epochMillies-fields + const featureCollection = hitsToGeoJson( + // @ts-expect-error + result.hits.hits, + (hit: Record) => { + return flattenHit(geometryFieldName, hit); + }, + geometryFieldName, + ES_GEO_FIELD_TYPE.GEO_SHAPE, + [] + ); + + resultFeatures = featureCollection.features; + + // Correct system-fields. + for (let i = 0; i < resultFeatures.length; i++) { + const props = resultFeatures[i].properties; + if (props !== null) { + props[FEATURE_ID_PROPERTY_NAME] = resultFeatures[i].id; + } + } + } + } catch (e) { + logger.warn(e.message); + throw e; + } + + const featureCollection: FeatureCollection = { + features: resultFeatures, + type: 'FeatureCollection', + }; + + const tileIndex = geojsonvt(featureCollection, { + maxZoom: 24, // max zoom to preserve detail on; can't be higher than 24 + tolerance: 3, // simplification tolerance (higher means simpler) + extent: 4096, // tile extent (both width and height) + buffer: 64, // tile buffer on each side + debug: 0, // logging level (0 to disable, 1 or 2) + lineMetrics: false, // whether to enable line metrics tracking for LineString/MultiLineString features + promoteId: null, // name of a feature property to promote to feature.id. Cannot be used with `generateId` + generateId: false, // whether to generate feature ids. Cannot be used with `promoteId` + indexMaxZoom: 5, // max zoom in the initial tile index + indexMaxPoints: 100000, // max number of points per tile in the index + }); + const tile = tileIndex.getTile(z, x, y); + + if (tile) { + const pbf = vtpbf.fromGeojsonVt({ [MVT_SOURCE_LAYER_NAME]: tile }, { version: 2 }); + return Buffer.from(pbf); + } else { + return null; + } + } catch (e) { + logger.warn(`Cannot generate tile for ${z}/${x}/${y}: ${e.message}`); + return null; + } +} + +function tileToGeoJsonPolygon(x: number, y: number, z: number): Polygon { + const wLon = tile2long(x, z); + const sLat = tile2lat(y + 1, z); + const eLon = tile2long(x + 1, z); + const nLat = tile2lat(y, z); + + return { + type: 'Polygon', + coordinates: [ + [ + [wLon, sLat], + [wLon, nLat], + [eLon, nLat], + [eLon, sLat], + [wLon, sLat], + ], + ], + }; +} + +function tile2long(x: number, z: number): number { + return (x / Math.pow(2, z)) * 360 - 180; +} + +function tile2lat(y: number, z: number): number { + const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z); + return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); +} + +function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon { + let minLon = esBounds.top_left.lon; + const maxLon = esBounds.bottom_right.lon; + minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline + const minLat = esBounds.bottom_right.lat; + const maxLat = esBounds.top_left.lat; + + return { + type: 'Polygon', + coordinates: [ + [ + [minLon, minLat], + [minLon, maxLat], + [maxLon, maxLat], + [maxLon, minLat], + [minLon, minLat], + ], + ], + }; +} diff --git a/x-pack/plugins/maps/server/mvt/mvt_routes.ts b/x-pack/plugins/maps/server/mvt/mvt_routes.ts new file mode 100644 index 0000000000000..32c14a355ba2a --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/mvt_routes.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import rison from 'rison-node'; +import { schema } from '@kbn/config-schema'; +import { Logger } from 'src/core/server'; +import { IRouter } from 'src/core/server'; +import { MVT_GETTILE_API_PATH, API_ROOT_PATH } from '../../common/constants'; +import { getTile } from './get_tile'; + +const CACHE_TIMEOUT = 0; // Todo. determine good value. Unsure about full-implications (e.g. wrt. time-based data). + +export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRouter }) { + router.get( + { + path: `${API_ROOT_PATH}/${MVT_GETTILE_API_PATH}`, + validate: { + query: schema.object({ + x: schema.number(), + y: schema.number(), + z: schema.number(), + geometryFieldName: schema.string(), + requestBody: schema.string(), + index: schema.string(), + }), + }, + }, + async (context, request, response) => { + const { query } = request; + + const callElasticsearch = async (type: string, ...args: any[]): Promise => { + return await context.core.elasticsearch.legacy.client.callAsCurrentUser(type, ...args); + }; + + const requestBodyDSL = rison.decode(query.requestBody); + + const tile = await getTile({ + logger, + callElasticsearch, + geometryFieldName: query.geometryFieldName, + x: query.x, + y: query.y, + z: query.z, + index: query.index, + requestBody: requestBodyDSL, + }); + + if (tile) { + return response.ok({ + body: tile, + headers: { + 'content-disposition': 'inline', + 'content-length': `${tile.length}`, + 'Content-Type': 'application/x-protobuf', + 'Cache-Control': `max-age=${CACHE_TIMEOUT}`, + }, + }); + } else { + return response.ok({ + headers: { + 'content-disposition': 'inline', + 'content-length': '0', + 'Content-Type': 'application/x-protobuf', + 'Cache-Control': `max-age=${CACHE_TIMEOUT}`, + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/maps/server/mvt/util.ts b/x-pack/plugins/maps/server/mvt/util.ts new file mode 100644 index 0000000000000..eb85468dd770d --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/util.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. + */ + +// This implementation: +// - does not include meta-fields +// - does not validate the schema against the index-pattern (e.g. nested fields) +// In the context of .mvt this is sufficient: +// - only fields from the response are packed in the tile (more efficient) +// - query-dsl submitted from the client, which was generated by the IndexPattern +// todo: Ideally, this should adapt/reuse from https://github.com/elastic/kibana/blob/52b42a81faa9dd5c102b9fbb9a645748c3623121/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts#L26 +import { GeoJsonProperties } from 'geojson'; + +export function flattenHit(geometryField: string, hit: Record): GeoJsonProperties { + const flat: GeoJsonProperties = {}; + if (hit) { + flattenSource(flat, '', hit._source as Record, geometryField); + if (hit.fields) { + flattenFields(flat, hit.fields as Array>); + } + + // Attach meta fields + flat._index = hit._index; + flat._id = hit._id; + } + return flat; +} + +function flattenSource( + accum: GeoJsonProperties, + path: string, + properties: Record = {}, + geometryField: string +): GeoJsonProperties { + accum = accum || {}; + for (const key in properties) { + if (properties.hasOwnProperty(key)) { + const newKey = path ? path + '.' + key : key; + let value; + if (geometryField === newKey) { + value = properties[key]; // do not deep-copy the geometry + } else if (properties[key] !== null && typeof value === 'object' && !Array.isArray(value)) { + value = flattenSource( + accum, + newKey, + properties[key] as Record, + geometryField + ); + } else { + value = properties[key]; + } + accum[newKey] = value; + } + } + return accum; +} + +function flattenFields(accum: GeoJsonProperties = {}, fields: Array>) { + accum = accum || {}; + for (const key in fields) { + if (fields.hasOwnProperty(key)) { + const value = fields[key]; + if (Array.isArray(value)) { + accum[key] = value[0]; + } else { + accum[key] = value; + } + } + } +} diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 7d091099c1aaa..6862e7536b07f 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -27,6 +27,7 @@ import { ILicense } from '../../licensing/common/types'; import { LicensingPluginSetup } from '../../licensing/server'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { MapsLegacyPluginSetup } from '../../../../src/plugins/maps_legacy/server'; +import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; interface SetupDeps { features: FeaturesPluginSetupContract; @@ -50,7 +51,7 @@ export class MapsPlugin implements Plugin { _initHomeData( home: HomeServerPluginSetup, prependBasePath: (path: string) => string, - mapConfig: any + mapsLegacyConfig: MapsLegacyConfig ) { const sampleDataLinkLabel = i18n.translate('xpack.maps.sampleDataLinkLabel', { defaultMessage: 'Map', @@ -123,7 +124,7 @@ export class MapsPlugin implements Plugin { home.tutorials.registerTutorial( emsBoundariesSpecProvider({ prependBasePath, - emsLandingPageUrl: mapConfig.emsLandingPageUrl, + emsLandingPageUrl: mapsLegacyConfig.emsLandingPageUrl, }) ); } @@ -160,7 +161,7 @@ export class MapsPlugin implements Plugin { } }); - this._initHomeData(home, core.http.basePath.prepend, currentConfig); + this._initHomeData(home, core.http.basePath.prepend, mapsLegacyConfig); features.registerFeature({ id: APP_ID, diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index 1876c0de19c56..6b19103b59722 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -22,6 +22,7 @@ import { EMS_SPRITES_PATH, INDEX_SETTINGS_API_PATH, FONTS_API_PATH, + API_ROOT_PATH, } from '../common/constants'; import { EMSClient } from '@elastic/ems-client'; import fetch from 'node-fetch'; @@ -30,8 +31,7 @@ import { getIndexPatternSettings } from './lib/get_index_pattern_settings'; import { schema } from '@kbn/config-schema'; import fs from 'fs'; import path from 'path'; - -const ROOT = `/${GIS_API_PATH}`; +import { initMVTRoutes } from './mvt/mvt_routes'; export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { let emsClient; @@ -69,7 +69,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_FILES_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}`, + path: `${API_ROOT_PATH}/${EMS_FILES_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}`, validate: { query: schema.object({ id: schema.maybe(schema.string()), @@ -109,7 +109,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`, validate: false, }, async (context, request, response) => { @@ -145,7 +145,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_CATALOGUE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_CATALOGUE_PATH}`, validate: false, }, async (context, request, { ok, badRequest }) => { @@ -181,7 +181,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`, + path: `${API_ROOT_PATH}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`, validate: false, }, async (context, request, { ok, badRequest }) => { @@ -213,7 +213,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`, + path: `${API_ROOT_PATH}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`, validate: false, }, async (context, request, { ok, badRequest }) => { @@ -257,7 +257,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}`, validate: { query: schema.object({ id: schema.maybe(schema.string()), @@ -293,7 +293,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}`, validate: { query: schema.object({ id: schema.string(), @@ -341,7 +341,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}`, validate: { query: schema.object({ id: schema.string(), @@ -379,7 +379,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}`, validate: { query: schema.object({ id: schema.string(), @@ -417,7 +417,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`, validate: { params: schema.object({ fontstack: schema.string(), @@ -439,7 +439,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { router.get( { - path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_SPRITES_PATH}/{id}/sprite{scaling?}.{extension}`, + path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_SPRITES_PATH}/{id}/sprite{scaling?}.{extension}`, validate: { query: schema.object({ elastic_tile_service_tos: schema.maybe(schema.string()), @@ -591,4 +591,6 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return response.badRequest(`Cannot connect to EMS`); } } + + initMVTRoutes({ router, logger }); } diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts index be15120cb19e1..867daddfd7b03 100644 --- a/x-pack/plugins/maps/server/tutorials/ems/index.ts +++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts @@ -60,7 +60,7 @@ Indexing EMS administrative boundaries in Elasticsearch allows for search on bou }), textPre: i18n.translate('xpack.maps.tutorials.ems.uploadStepText', { defaultMessage: - '1. Open [Elastic Maps]({newMapUrl}).\n\ + '1. Open [Maps]({newMapUrl}).\n\ 2. Click `Add layer`, then select `Upload GeoJSON`.\n\ 3. Upload the GeoJSON file and click `Import file`.', values: { 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/constants/settings.ts b/x-pack/plugins/ml/common/constants/settings.ts index 2df2ecd22e078..bab2aa2f2a0ae 100644 --- a/x-pack/plugins/ml/common/constants/settings.ts +++ b/x-pack/plugins/ml/common/constants/settings.ts @@ -5,3 +5,11 @@ */ export const FILE_DATA_VISUALIZER_MAX_FILE_SIZE = 'ml:fileDataVisualizerMaxFileSize'; +export const ANOMALY_DETECTION_ENABLE_TIME_RANGE = 'ml:anomalyDetection:results:enableTimeDefaults'; +export const ANOMALY_DETECTION_DEFAULT_TIME_RANGE = 'ml:anomalyDetection:results:timeDefaults'; + +export const DEFAULT_AD_RESULTS_TIME_FILTER = { + from: 'now-15m', + to: 'now', +}; +export const DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER = false; 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/custom_hooks/use_create_ad_links.ts b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts new file mode 100644 index 0000000000000..368e758a027c4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.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. + */ +import { useCallback } from 'react'; +import { useMlKibana, useUiSettings } from '../../contexts/kibana'; +import { + ANOMALY_DETECTION_DEFAULT_TIME_RANGE, + ANOMALY_DETECTION_ENABLE_TIME_RANGE, +} from '../../../../common/constants/settings'; +import { mlJobService } from '../../services/job_service'; + +export const useCreateADLinks = () => { + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); + + const useUserTimeSettings = useUiSettings().get(ANOMALY_DETECTION_ENABLE_TIME_RANGE); + const userTimeSettings = useUiSettings().get(ANOMALY_DETECTION_DEFAULT_TIME_RANGE); + const createLinkWithUserDefaults = useCallback( + (location, jobList) => { + const resultsPageUrl = mlJobService.createResultsUrlForJobs( + jobList, + location, + useUserTimeSettings === true && userTimeSettings !== undefined + ? userTimeSettings + : undefined + ); + return `${basePath.get()}/app/ml${resultsPageUrl}`; + }, + [basePath] + ); + return { createLinkWithUserDefaults }; +}; 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/components/job_selector/job_selector_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx index 62a74ed142ccf..6c57b3d08180d 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx @@ -237,7 +237,7 @@ export const JobSelectorFlyout: FC = ({ } - > -

    - -

    - + /> `; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js index 9fcd457df008f..5140fe77ff979 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js @@ -47,7 +47,7 @@ export class DeleteRuleModal extends Component { title={ } onCancel={this.closeModal} @@ -66,14 +66,7 @@ export class DeleteRuleModal extends Component { /> } defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

    - -

    - + /> ); } 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/action_delete/delete_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx index 5ffa5e304b996..5db8446dec32f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx @@ -14,7 +14,6 @@ import { EuiFlexItem, EUI_MODAL_CONFIRM_BUTTON, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { DeleteAction } from './use_delete_action'; @@ -40,7 +39,7 @@ export const DeleteActionModal: FC = ({ = ({ defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} buttonColor="danger" > -

    - -

    - {userCanDeleteIndex && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx index fa559e807f5ea..2048d1144952d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx @@ -17,7 +17,7 @@ export const StartActionModal: FC = ({ closeModal, item, startAndCl = ({ closeModal, item, startAndCl

    {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { defaultMessage: - 'A data frame analytics job will increase search and indexing load in your cluster. Please stop the analytics job if excessive load is experienced. Are you sure you want to start this analytics job?', + 'A data frame analytics job increases search and indexing load in your cluster. If excessive load occurs, stop the job.', })}

    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/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx index 83010c684473e..3c2ba13a1db26 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx @@ -15,7 +15,6 @@ import { EuiModalFooter, EuiButtonEmpty, EuiButton, - EuiSpacer, EuiCallOut, } from '@elastic/eui'; import { ModelItemFull } from './models_list'; @@ -37,7 +36,7 @@ export const DeleteModelsModal: FC = ({ models, onClose = ({ models, onClose - - {modelsWithPipelines.length > 0 && ( = ({ 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.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index d78df80fad94e..cea1159ebc14a 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -68,6 +68,7 @@ import { ExplorerChartsContainer } from './explorer_charts/explorer_charts_conta import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; import { getTimefilter, getToastNotifications } from '../util/dependency_cache'; +import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings'; const ExplorerPage = ({ children, @@ -145,6 +146,22 @@ export class Explorer extends React.Component { state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG }; htmlIdGen = htmlIdGenerator(); + componentDidMount() { + const { invalidTimeRangeError } = this.props; + if (invalidTimeRangeError) { + const toastNotifications = getToastNotifications(); + toastNotifications.addWarning( + i18n.translate('xpack.ml.explorer.invalidTimeRangeInUrlCallout', { + defaultMessage: + 'The time filter was changed to the full range due to an invalid default time filter. Check the advanced settings for {field}.', + values: { + field: ANOMALY_DETECTION_DEFAULT_TIME_RANGE, + }, + }) + ); + } + } + // Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes // and will cause a syntax error when called with getKqlQueryValues applyFilter = (fieldName, fieldValue, action) => { @@ -298,7 +315,6 @@ export class Explorer extends React.Component {
    - {stoppedPartitions && ( = 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/delete_job_modal/delete_job_modal.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js index 1e3ec6241311b..f80578cb18341 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js @@ -98,7 +98,7 @@ export class DeleteJobModal extends Component { const title = ( -

    - -

    {singleMetricVisible && ( )}

    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/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx index 07a555c13dbf5..a71141d0356d0 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx @@ -7,9 +7,8 @@ import React, { FC } from 'react'; import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -// @ts-ignore no module file -import { getLink } from '../../../jobs/jobs_list/components/job_actions/results'; import { MlSummaryJobs } from '../../../../../common/types/anomaly_detection_jobs'; +import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links'; interface Props { jobsList: MlSummaryJobs; @@ -23,13 +22,14 @@ export const ExplorerLink: FC = ({ jobsList }) => { values: { jobsCount: jobsList.length, jobId: jobsList[0] && jobsList[0].id }, } ); + const { createLinkWithUserDefaults } = useCreateADLinks(); return ( = ({ 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..f89e27925d745 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'; @@ -72,7 +73,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [globalState, setGlobalState] = useUrlState('_g'); const [lastRefresh, setLastRefresh] = useState(0); const [stoppedPartitions, setStoppedPartitions] = useState(); - + const [invalidTimeRangeError, setInValidTimeRangeError] = useState(false); const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true }); const { jobIds } = useJobSelection(jobsWithTimeRange); @@ -98,6 +99,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim // `timefilter.getBounds()` to update `bounds` in this component's state. useEffect(() => { if (globalState?.time !== undefined) { + if (globalState.time.mode === 'invalid') { + setInValidTimeRangeError(true); + } timefilter.setTime({ from: globalState.time.from, to: globalState.time.to, @@ -235,6 +239,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim showCharts, severity: tableSeverity.val, stoppedPartitions, + invalidTimeRangeError, }} />
    diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 1f122ed18a851..817c975415997 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -91,6 +91,7 @@ export const TimeSeriesExplorerUrlStateManager: FC(); const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true }); + const [invalidTimeRangeError, setInValidTimeRangeError] = useState(false); const refresh = useRefresh(); useEffect(() => { @@ -114,6 +115,9 @@ export const TimeSeriesExplorerUrlStateManager: FC(undefined); useEffect(() => { if (globalState?.time !== undefined) { + if (globalState.time.mode === 'invalid') { + setInValidTimeRangeError(true); + } timefilter.setTime({ from: globalState.time.from, to: globalState.time.to, @@ -300,6 +304,7 @@ export const TimeSeriesExplorerUrlStateManager: FC ); 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/services/job_service.d.ts b/x-pack/plugins/ml/public/application/services/job_service.d.ts index 2134d157e1baa..30b2ec044285a 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/job_service.d.ts @@ -5,6 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; +import { TimeRange } from 'src/plugins/data/common/query/timefilter/types'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { Calendar } from '../../../common/types/calendars'; @@ -15,7 +16,7 @@ export interface ExistingJobsAndGroups { declare interface JobService { jobs: CombinedJob[]; - createResultsUrlForJobs: (jobs: any[], target: string) => string; + createResultsUrlForJobs: (jobs: any[], target: string, timeRange?: TimeRange) => string; tempJobCloningObjects: { job: any; skipTimeRangeStep: boolean; diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 704d76059f75c..640f63617b7d4 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -21,7 +21,7 @@ import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; import { TIME_FORMAT } from '../../../common/constants/time_format'; import { parseInterval } from '../../../common/util/parse_interval'; import { toastNotificationServiceProvider } from '../services/toast_notification_service'; - +import { validateTimeRange } from '../util/date_utils'; const msgs = mlMessageBarService; let jobs = []; let datafeedIds = {}; @@ -790,8 +790,8 @@ class JobService { return groups; } - createResultsUrlForJobs(jobsList, resultsPage) { - return createResultsUrlForJobs(jobsList, resultsPage); + createResultsUrlForJobs(jobsList, resultsPage, timeRange) { + return createResultsUrlForJobs(jobsList, resultsPage, timeRange); } createResultsUrl(jobIds, from, to, resultsPage) { @@ -932,31 +932,54 @@ function createJobStats(jobsList, jobStats) { jobStats.activeNodes.value = Object.keys(mlNodes).length; } -function createResultsUrlForJobs(jobsList, resultsPage) { +function createResultsUrlForJobs(jobsList, resultsPage, userTimeRange) { let from = undefined; let to = undefined; - if (jobsList.length === 1) { - from = jobsList[0].earliestTimestampMs; - to = jobsList[0].latestResultsTimestampMs; // Will be max(latest source data, latest bucket results) + let mode = 'absolute'; + const jobIds = jobsList.map((j) => j.id); + + // if the custom default time filter is set and enabled in advanced settings + // if time is either absolute date or proper datemath format + if (validateTimeRange(userTimeRange)) { + from = userTimeRange.from; + to = userTimeRange.to; + // if both pass datemath's checks but are not technically absolute dates, use 'quick' + // e.g. "now-15m" "now+1d" + const fromFieldAValidDate = moment(userTimeRange.from).isValid(); + const toFieldAValidDate = moment(userTimeRange.to).isValid(); + if (!fromFieldAValidDate && !toFieldAValidDate) { + return createResultsUrl(jobIds, from, to, resultsPage, 'quick'); + } } else { - const jobsWithData = jobsList.filter((j) => j.earliestTimestampMs !== undefined); - if (jobsWithData.length > 0) { - from = Math.min(...jobsWithData.map((j) => j.earliestTimestampMs)); - to = Math.max(...jobsWithData.map((j) => j.latestResultsTimestampMs)); + // if time range is specified but with incorrect format + // change back to the default time range but alert the user + // that the advanced setting config is invalid + if (userTimeRange) { + mode = 'invalid'; + } + + if (jobsList.length === 1) { + from = jobsList[0].earliestTimestampMs; + to = jobsList[0].latestResultsTimestampMs; // Will be max(latest source data, latest bucket results) + } else { + const jobsWithData = jobsList.filter((j) => j.earliestTimestampMs !== undefined); + if (jobsWithData.length > 0) { + from = Math.min(...jobsWithData.map((j) => j.earliestTimestampMs)); + to = Math.max(...jobsWithData.map((j) => j.latestResultsTimestampMs)); + } } } const fromString = moment(from).format(TIME_FORMAT); // Defaults to 'now' if 'from' is undefined const toString = moment(to).format(TIME_FORMAT); // Defaults to 'now' if 'to' is undefined - const jobIds = jobsList.map((j) => j.id); - return createResultsUrl(jobIds, fromString, toString, resultsPage); + return createResultsUrl(jobIds, fromString, toString, resultsPage, mode); } -function createResultsUrl(jobIds, start, end, resultsPage) { +function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') { const idString = jobIds.map((j) => `'${j}'`).join(','); - const from = moment(start).toISOString(); - const to = moment(end).toISOString(); + let from; + let to; let path = ''; if (resultsPage !== undefined) { @@ -964,9 +987,20 @@ function createResultsUrl(jobIds, start, end, resultsPage) { path += resultsPage; } + if (mode === 'quick') { + from = start; + to = end; + } else { + from = moment(start).toISOString(); + to = moment(end).toISOString(); + } + path += `?_g=(ml:(jobIds:!(${idString}))`; path += `,refreshInterval:(display:Off,pause:!f,value:0),time:(from:'${from}'`; - path += `,mode:absolute,to:'${to}'`; + path += `,to:'${to}'`; + if (mode === 'invalid') { + path += `,mode:invalid`; + } path += "))&_a=(query:(query_string:(analyze_wildcard:!t,query:'*')))"; return path; 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/components/delete_filter_list_modal/delete_filter_list_modal.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js index 7f8bf06ecb8a3..75fdce8e2bac8 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js @@ -58,7 +58,7 @@ export class DeleteFilterListModal extends Component { const title = ( -

    - -

    - + /> ); } @@ -114,6 +103,7 @@ export class DeleteFilterListModal extends Component { selectedFilterLists.length === 0 || canDeleteFilter === false } + data-test-subj="mlFilterListsDeleteButton" > ); 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 ( - + { diff --git a/x-pack/plugins/ml/public/application/util/date_utils.ts b/x-pack/plugins/ml/public/application/util/date_utils.ts index 8f3215b6cd211..21adc0b4b9c66 100644 --- a/x-pack/plugins/ml/public/application/util/date_utils.ts +++ b/x-pack/plugins/ml/public/application/util/date_utils.ts @@ -8,7 +8,8 @@ // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; - +import dateMath from '@elastic/datemath'; +import { TimeRange } from '../../../../../../src/plugins/data/common'; export function formatHumanReadableDate(ts: number) { return formatDate(ts, 'MMMM Do YYYY'); } @@ -20,3 +21,10 @@ export function formatHumanReadableDateTime(ts: number): string { export function formatHumanReadableDateTimeSeconds(ts: number) { return formatDate(ts, 'MMMM Do YYYY, HH:mm:ss'); } + +export function validateTimeRange(time?: TimeRange): boolean { + if (!time) return false; + const momentDateFrom = dateMath.parse(time.from); + const momentDateTo = dateMath.parse(time.to); + return !!(momentDateFrom && momentDateFrom.isValid() && momentDateTo && momentDateTo.isValid()); +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts new file mode 100644 index 0000000000000..c4aebb108e7b9 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import isEmpty from 'lodash/isEmpty'; +import { + AnomalyDetectionQueryState, + AnomalyDetectionUrlState, + ExplorerAppState, + ExplorerGlobalState, + ExplorerUrlState, + MlGenericUrlState, + TimeSeriesExplorerAppState, + TimeSeriesExplorerGlobalState, + TimeSeriesExplorerUrlState, +} from '../../common/types/ml_url_generator'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; +import { createIndexBasedMlUrl } from './common'; +import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; +/** + * Creates URL to the Anomaly Detection Job management page + */ +export function createAnomalyDetectionJobManagementUrl( + appBasePath: string, + params: AnomalyDetectionUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE}`; + if (!params || isEmpty(params)) { + return url; + } + const { jobId, groupIds } = params; + const queryState: AnomalyDetectionQueryState = { + jobId, + groupIds, + }; + + url = setStateToKbnUrl( + '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 4f8ceb8effe98..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'; @@ -96,12 +96,7 @@ export class MlPlugin implements Plugin { uiActions: pluginsStart.uiActions, kibanaVersion, }, - { - element: params.element, - appBasePath: params.appBasePath, - onAppLeave: params.onAppLeave, - history: params.history, - } + params ); }, }); 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/register_settings.ts b/x-pack/plugins/ml/server/lib/register_settings.ts index 38b1f5e3fc083..a9ee24fbb5cea 100644 --- a/x-pack/plugins/ml/server/lib/register_settings.ts +++ b/x-pack/plugins/ml/server/lib/register_settings.ts @@ -7,7 +7,13 @@ import { CoreSetup } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { FILE_DATA_VISUALIZER_MAX_FILE_SIZE } from '../../common/constants/settings'; +import { + FILE_DATA_VISUALIZER_MAX_FILE_SIZE, + ANOMALY_DETECTION_DEFAULT_TIME_RANGE, + ANOMALY_DETECTION_ENABLE_TIME_RANGE, + DEFAULT_AD_RESULTS_TIME_FILTER, + DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER, +} from '../../common/constants/settings'; import { MAX_FILE_SIZE } from '../../common/constants/file_datavisualizer'; export function registerKibanaSettings(coreSetup: CoreSetup) { @@ -30,5 +36,40 @@ export function registerKibanaSettings(coreSetup: CoreSetup) { }), }, }, + [ANOMALY_DETECTION_ENABLE_TIME_RANGE]: { + name: i18n.translate('xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeName', { + defaultMessage: 'Enable time filter defaults for anomaly detection results', + }), + value: DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER, + schema: schema.boolean(), + description: i18n.translate( + 'xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeDesc', + { + defaultMessage: + 'Use the default time filter in the Single Metric Viewer and Anomaly Explorer. If not enabled, the results for the full time range of the job are displayed.', + } + ), + category: ['Machine Learning'], + }, + [ANOMALY_DETECTION_DEFAULT_TIME_RANGE]: { + name: i18n.translate('xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeName', { + defaultMessage: 'Time filter defaults for anomaly detection results', + }), + type: 'json', + value: JSON.stringify(DEFAULT_AD_RESULTS_TIME_FILTER, null, 2), + description: i18n.translate( + 'xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeDesc', + { + defaultMessage: + 'The time filter selection to use when viewing anomaly detection job results.', + } + ), + schema: schema.object({ + from: schema.string(), + to: schema.string(), + }), + requiresPageReload: true, + category: ['Machine Learning'], + }, }); } 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.ts b/x-pack/plugins/ml/server/shared_services/license_checks.ts deleted file mode 100644 index 191124ffa5f3a..0000000000000 --- a/x-pack/plugins/ml/server/shared_services/license_checks.ts +++ /dev/null @@ -1,26 +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 { MlServerLicense } from '../lib/license'; - -export type LicenseCheck = () => void; - -export function licenseChecks( - mlLicense: MlServerLicense -): { isFullLicense: LicenseCheck; isMinimumLicense: LicenseCheck } { - return { - isFullLicense() { - if (mlLicense.isFullLicense() === false) { - throw Error('Platinum, Enterprise or trial license needed'); - } - }, - isMinimumLicense() { - if (mlLicense.isMinimumLicense() === false) { - throw Error('Basic license needed'); - } - }, - }; -} 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/license_checks.ts b/x-pack/plugins/ml/server/shared_services/license_checks/license_checks.ts new file mode 100644 index 0000000000000..3d9de1ef70f2d --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/license_checks/license_checks.ts @@ -0,0 +1,27 @@ +/* + * 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 { MlServerLicense } from '../../lib/license'; +import { InsufficientFullLicenseError, InsufficientBasicLicenseError } from './errors'; + +export type LicenseCheck = () => void; + +export function licenseChecks( + mlLicense: MlServerLicense +): { isFullLicense: LicenseCheck; isMinimumLicense: LicenseCheck } { + return { + isFullLicense() { + if (mlLicense.isFullLicense() === false) { + throw new InsufficientFullLicenseError('Platinum, Enterprise or trial license needed'); + } + }, + isMinimumLicense() { + if (mlLicense.isMinimumLicense() === false) { + 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/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap index c2ee498d7189d..e35d2ba6108f5 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap @@ -142,6 +142,8 @@ exports[`CcrShard that it renders normally 1`] = ` } id="ccrLatestStat" initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} paddingSize="l" > { + this.kibanaStatus = nextStatus.level; + }); } filterCollectorSet(usageCollection) { @@ -128,7 +128,7 @@ export class BulkUploader { async _fetchAndUpload(usageCollection) { const collectorsReady = await usageCollection.areAllCollectorsReady(); const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector); - if (!collectorsReady || typeof this.kibanaStatusGetter !== 'function') { + if (!collectorsReady) { this._log.debug('Skipping bulk uploading because not all collectors are ready'); if (hasUsageCollectors) { this._lastFetchUsageTime = null; @@ -172,10 +172,23 @@ export class BulkUploader { return await sendBulkPayload(this._cluster, this._interval, payload, this._log); } + getConvertedKibanaStatuss() { + if (this.kibanaStatus === ServiceStatusLevels.available) { + return 'green'; + } + if (this.kibanaStatus === ServiceStatusLevels.critical) { + return 'red'; + } + if (this.kibanaStatus === ServiceStatusLevels.degraded) { + return 'yellow'; + } + return 'unknown'; + } + getKibanaStats(type) { const stats = { ...this.kibanaStats, - status: this.kibanaStatusGetter(), + status: this.getConvertedKibanaStatuss(), }; if (type === KIBANA_STATS_TYPE_MONITORING) { diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 8bd43b6be0a8d..a2520593c436d 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.ts @@ -5,8 +5,6 @@ */ import { Plugin } from './plugin'; import { combineLatest } from 'rxjs'; -// @ts-ignore -import { initBulkUploader } from './kibana_monitoring'; import { AlertsFactory } from './alerts'; jest.mock('rxjs', () => ({ @@ -27,10 +25,6 @@ jest.mock('./license_service', () => ({ })), })); -jest.mock('./kibana_monitoring', () => ({ - initBulkUploader: jest.fn(), -})); - describe('Monitoring plugin', () => { const initializerContext = { logger: { @@ -71,6 +65,11 @@ describe('Monitoring plugin', () => { createClient: jest.fn(), }, }, + status: { + overall$: { + subscribe: jest.fn(), + }, + }, }; const setupPlugins = { @@ -113,19 +112,13 @@ describe('Monitoring plugin', () => { afterEach(() => { (setupPlugins.alerts.registerType as jest.Mock).mockReset(); + (coreSetup.status.overall$.subscribe as jest.Mock).mockReset(); }); it('always create the bulk uploader', async () => { - const setKibanaStatusGetter = jest.fn(); - (initBulkUploader as jest.Mock).mockImplementation(() => { - return { - setKibanaStatusGetter, - }; - }); const plugin = new Plugin(initializerContext as any); - const contract = await plugin.setup(coreSetup as any, setupPlugins as any); - contract.registerLegacyAPI(null as any); - expect(setKibanaStatusGetter).toHaveBeenCalled(); + await plugin.setup(coreSetup as any, setupPlugins as any); + expect(coreSetup.status.overall$.subscribe).toHaveBeenCalled(); }); it('should register all alerts', async () => { diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 501c96b12fde8..f5cbadb523a81 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -46,7 +46,6 @@ import { IBulkUploader, PluginsSetup, PluginsStart, - LegacyAPI, LegacyRequest, } from './types'; @@ -158,6 +157,7 @@ export class Plugin { elasticsearch: core.elasticsearch, config, log: kibanaMonitoringLog, + statusGetter$: core.status.overall$, kibanaStats: { uuid: this.initializerContext.env.instanceUuid, name: serverInfo.name, @@ -221,11 +221,6 @@ export class Plugin { } return { - // The legacy plugin calls this to register certain legacy dependencies - // that are necessary for the plugin to properly run - registerLegacyAPI: (legacyAPI: LegacyAPI) => { - this.setupLegacy(legacyAPI); - }, // OSS stats api needs to call this in order to centralize how // we fetch kibana specific stats getKibanaStats: () => this.bulkUploader.getKibanaStats(), @@ -280,11 +275,6 @@ export class Plugin { }); } - async setupLegacy(legacyAPI: LegacyAPI) { - // Set the stats getter - this.bulkUploader.setKibanaStatusGetter(() => legacyAPI.getServerStatus()); - } - getLegacyShim( config: MonitoringConfig, legacyConfig: any, diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index a0ef6d3e2d984..e6a4b174df55d 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -33,10 +33,6 @@ export interface MonitoringElasticsearchConfig { hosts: string[]; } -export interface LegacyAPI { - getServerStatus: () => string; -} - export interface PluginsSetup { encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup; @@ -77,7 +73,6 @@ export interface LegacyShimDependencies { } export interface IBulkUploader { - setKibanaStatusGetter: (getter: () => string | undefined) => void; getKibanaStats: () => any; } diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index db7fca140be89..19995ed233e8d 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createMemoryHistory } from 'history'; import React from 'react'; -import { renderApp } from './'; import { Observable } from 'rxjs'; -import { CoreStart, AppMountParameters } from 'src/core/public'; +import { AppMountParameters, CoreStart } from 'src/core/public'; +import { renderApp } from './'; describe('renderApp', () => { it('renders', () => { @@ -19,6 +20,7 @@ describe('renderApp', () => { } as unknown) as CoreStart; const params = ({ element: window.document.createElement('div'), + history: createMemoryHistory(), } as unknown) as AppMountParameters; expect(() => { diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 4c0147dc3cd51..fa691a7f41ddb 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { createHashHistory } from 'history'; import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; @@ -52,10 +51,10 @@ function App() { ); } -export const renderApp = (core: CoreStart, { element }: AppMountParameters) => { +export const renderApp = (core: CoreStart, { element, history }: AppMountParameters) => { const i18nCore = core.i18n; const isDarkMode = core.uiSettings.get('theme:darkMode'); - const history = createHashHistory(); + ReactDOM.render( diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index a1d51ffda6afd..b635c2c68b926 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -112,7 +112,7 @@ export function APMSection({ absoluteTime, relativeTime, bucketSize }: Props) { `${formatTpm(value)} tpm`} /> diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx index aa1dc1640125e..343611294bc45 100644 --- a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -150,7 +150,7 @@ export function LogsSection({ absoluteTime, relativeTime, bucketSize }: Props) { /> numeral(d).format('0a')} /> diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx index cfb06af3224c7..5c23c7a065b5e 100644 --- a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -182,7 +182,7 @@ function UptimeBarSeries({ /> numeral(x).format('0a')} /> 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/generate_csv/hit_iterator.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts index 831bf45cf72ea..b7147fe0a9ebd 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.test.ts @@ -7,17 +7,13 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { CancellationToken } from '../../../../common'; -import { LevelLogger } from '../../../lib'; +import { createMockLevelLogger } from '../../../test_helpers/create_mock_levellogger'; import { ScrollConfig } from '../../../types'; import { createHitIterator } from './hit_iterator'; -const mockLogger = { - error: new Function(), - debug: new Function(), - warning: new Function(), -} as LevelLogger; +const mockLogger = createMockLevelLogger(); const debugLogStub = sinon.stub(mockLogger, 'debug'); -const warnLogStub = sinon.stub(mockLogger, 'warning'); +const warnLogStub = sinon.stub(mockLogger, 'warn'); const errorLogStub = sinon.stub(mockLogger, 'error'); const mockCallEndpoint = sinon.stub(); const mockSearchRequest = {}; @@ -134,4 +130,30 @@ describe('hitIterator', function () { expect(errorLogStub.callCount).to.be(1); expect(errorThrown).to.be(true); }); + + it('handles scroll id could not be cleared', async () => { + // Setup + mockCallEndpoint.withArgs('clearScroll').rejects({ status: 404 }); + + // Begin + const hitIterator = createHitIterator(mockLogger); + const iterator = hitIterator( + mockConfig, + mockCallEndpoint, + mockSearchRequest, + realCancellationToken + ); + + while (true) { + const { done: iterationDone, value: hit } = await iterator.next(); + if (iterationDone) { + break; + } + expect(hit).to.be('you found me'); + } + + expect(mockCallEndpoint.callCount).to.be(13); + expect(warnLogStub.callCount).to.be(1); + expect(errorLogStub.callCount).to.be(1); + }); }); diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts index dee653cf30007..b95a311200266 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts @@ -68,11 +68,17 @@ export function createHitIterator(logger: LevelLogger) { ); } - function clearScroll(scrollId: string | undefined) { + async function clearScroll(scrollId: string | undefined) { logger.debug('executing clearScroll request'); - return callEndpoint('clearScroll', { - scrollId: [scrollId], - }); + try { + await callEndpoint('clearScroll', { + scrollId: [scrollId], + }); + } catch (err) { + // Do not throw the error, as the job can still be completed successfully + logger.warn('Scroll context can not be cleared!'); + logger.error(err); + } } try { @@ -86,7 +92,7 @@ export function createHitIterator(logger: LevelLogger) { ({ scrollId, hits } = await scroll(scrollId)); if (cancellationToken.isCancelled()) { - logger.warning( + logger.warn( 'Any remaining scrolling searches have been cancelled by the cancellation token.' ); } 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/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index 420fa8347cdeb..e0d018869cef1 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -86,6 +86,15 @@ describe('Reporting Plugin', () => { expect(plugin.start(coreStart, pluginStart)).not.toHaveProperty('then'); }); + it('registers an advanced setting for PDF logos', async () => { + const plugin = new ReportingPlugin(initContext); + plugin.setup(coreSetup, pluginSetup); + expect(coreSetup.uiSettings.register).toHaveBeenCalled(); + expect(coreSetup.uiSettings.register.mock.calls[0][0]).toHaveProperty( + 'xpackReporting:customPdfLogo' + ); + }); + it('logs start issues', async () => { const plugin = new ReportingPlugin(initContext); // @ts-ignore overloading error logger diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 20e22c2db00e3..8c0e352aa06c5 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -4,16 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from '../common/constants'; import { ReportingCore } from './'; import { initializeBrowserDriverFactory } from './browsers'; import { buildConfig, ReportingConfigType } from './config'; -import { createQueueFactory, LevelLogger, runValidations, ReportingStore } from './lib'; +import { createQueueFactory, LevelLogger, ReportingStore, runValidations } from './lib'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; import { registerReportingUsageCollector } from './usage'; +const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6); + declare module 'src/core/server' { interface RequestHandlerContext { reporting?: ReportingStart | null; @@ -34,7 +39,7 @@ export class ReportingPlugin public setup(core: CoreSetup, plugins: ReportingSetupDeps) { // prevent throwing errors in route handlers about async deps not being initialized - core.http.registerRouteHandlerContext('reporting', () => { + core.http.registerRouteHandlerContext(PLUGIN_ID, () => { if (this.reportingCore.pluginIsStarted()) { return {}; // ReportingStart contract } else { @@ -42,6 +47,28 @@ export class ReportingPlugin } }); + core.uiSettings.register({ + [UI_SETTINGS_CUSTOM_PDF_LOGO]: { + name: i18n.translate('xpack.reporting.pdfFooterImageLabel', { + defaultMessage: 'PDF footer image', + }), + value: null, + description: i18n.translate('xpack.reporting.pdfFooterImageDescription', { + defaultMessage: `Custom image to use in the PDF's footer`, + }), + type: 'image', + schema: schema.nullable(schema.byteSize({ max: '200kb' })), + category: [PLUGIN_ID], + // Used client-side for size validation + validation: { + maxSize: { + length: kbToBase64Length(200), + description: '200 kB', + }, + }, + }, + }); + const { elasticsearch, http } = core; const { licensing, security } = plugins; const { initializerContext: initContext, reportingCore } = this; 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/account_management/account_management_app.test.ts b/x-pack/plugins/security/public/account_management/account_management_app.test.ts index 37b97a8472310..c41bd43872bee 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.test.ts +++ b/x-pack/plugins/security/public/account_management/account_management_app.test.ts @@ -54,6 +54,7 @@ describe('accountManagementApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts index 0e262e9089842..eafad74d2f0d8 100644 --- a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts @@ -48,6 +48,7 @@ describe('accessAgreementApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts index c5b9245414630..e6723085460f8 100644 --- a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts +++ b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts @@ -54,6 +54,7 @@ describe('captureURLApp', () => { element: document.createElement('div'), appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: (scopedHistoryMock.create() as unknown) as ScopedHistory, }); diff --git a/x-pack/plugins/security/public/authentication/logged_out/logged_out_app.test.ts b/x-pack/plugins/security/public/authentication/logged_out/logged_out_app.test.ts index 15d55136b405d..86a5d21f1b233 100644 --- a/x-pack/plugins/security/public/authentication/logged_out/logged_out_app.test.ts +++ b/x-pack/plugins/security/public/authentication/logged_out/logged_out_app.test.ts @@ -46,6 +46,7 @@ describe('loggedOutApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/login/login_app.test.ts b/x-pack/plugins/security/public/authentication/login/login_app.test.ts index a6e5a321ef6ec..5ae8afab9de23 100644 --- a/x-pack/plugins/security/public/authentication/login/login_app.test.ts +++ b/x-pack/plugins/security/public/authentication/login/login_app.test.ts @@ -51,6 +51,7 @@ describe('loginApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/logout/logout_app.test.ts b/x-pack/plugins/security/public/authentication/logout/logout_app.test.ts index 46b1083a2ed14..b7bfdf492305e 100644 --- a/x-pack/plugins/security/public/authentication/logout/logout_app.test.ts +++ b/x-pack/plugins/security/public/authentication/logout/logout_app.test.ts @@ -52,6 +52,7 @@ describe('logoutApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.test.ts b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.test.ts index 0eed1382c270b..6e0e06dd3dc44 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.test.ts +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.test.ts @@ -53,6 +53,7 @@ describe('overwrittenSessionApp', () => { element: containerMock, appBasePath: '', onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), history: scopedHistoryMock.create(), }); 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/server/authorization/authorization_service.ts b/x-pack/plugins/security/server/authorization/authorization_service.ts index 4190499cbd5f4..2dead301b298a 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.ts @@ -5,7 +5,7 @@ */ import { Subscription, Observable } from 'rxjs'; -import { UICapabilities } from 'ui/capabilities'; +import type { Capabilities as UICapabilities } from '../../../../../src/core/types'; import { LoggerFactory, KibanaRequest, diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index c126be1b07f6e..41d596d570fb9 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -5,7 +5,7 @@ */ import { flatten, isObject, mapValues } from 'lodash'; -import { UICapabilities } from 'ui/capabilities'; +import type { Capabilities as UICapabilities } from '../../../../../src/core/types'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { Feature } from '../../../features/server'; 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..498b561a818f2 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; @@ -283,6 +283,9 @@ export type Status = t.TypeOf; export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null }); export type JobStatus = t.TypeOf; +export const conflicts = t.keyof({ abort: null, proceed: null }); +export type Conflicts = t.TypeOf; + // TODO: Create a regular expression type or custom date math part type here export const to = t.string; export type To = t.TypeOf; @@ -338,7 +341,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/request/set_signal_status_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts index 1464896e50294..b039558d827bb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts @@ -6,13 +6,14 @@ import * as t from 'io-ts'; -import { signal_ids, signal_status_query, status } from '../common/schemas'; +import { conflicts, signal_ids, signal_status_query, status } from '../common/schemas'; export const setSignalsStatusSchema = t.intersection([ t.type({ status, }), t.partial({ + conflicts, signal_ids, query: signal_status_query, }), 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 507ce63c7b815..366bf7a1df1f2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -13,3 +13,5 @@ export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurre 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.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index 7aec8e15c317c..b0c769216732d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetTrustedAppsRequestSchema } from './trusted_apps'; +import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema } from './trusted_apps'; describe('When invoking Trusted Apps Schema', () => { describe('for GET List', () => { @@ -68,4 +68,180 @@ describe('When invoking Trusted Apps Schema', () => { }); }); }); + + describe('for POST Create', () => { + const getCreateTrustedAppItem = () => ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: 'windows', + entries: [ + { + field: 'path', + type: 'match', + operator: 'included', + value: 'c:/programs files/Anti-Virus', + }, + ], + }); + const body = PostTrustedAppCreateRequestSchema.body; + + it('should not error on a valid message', () => { + const bodyMsg = getCreateTrustedAppItem(); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + }); + + it('should validate `name` is required', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + name: undefined, + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `name` value to be non-empty', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + name: '', + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `description` as optional', () => { + const { description, ...bodyMsg } = getCreateTrustedAppItem(); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + }); + + it('should validate `description` to be non-empty if defined', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + description: '', + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `os` to to only accept known values', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + os: undefined, + }; + expect(() => body.validate(bodyMsg)).toThrow(); + + const bodyMsg2 = { + ...bodyMsg, + os: '', + }; + expect(() => body.validate(bodyMsg2)).toThrow(); + + const bodyMsg3 = { + ...bodyMsg, + os: 'winz', + }; + expect(() => body.validate(bodyMsg3)).toThrow(); + + ['linux', 'macos', 'windows'].forEach((os) => { + expect(() => { + body.validate({ + ...bodyMsg, + os, + }); + }).not.toThrow(); + }); + }); + + it('should validate `entries` as required', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: undefined, + }; + expect(() => body.validate(bodyMsg)).toThrow(); + + const { entries, ...bodyMsg2 } = getCreateTrustedAppItem(); + expect(() => body.validate(bodyMsg2)).toThrow(); + }); + + it('should validate `entries` to have at least 1 item', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + describe('when `entries` are defined', () => { + const getTrustedAppItemEntryItem = () => getCreateTrustedAppItem().entries[0]; + + it('should validate `entry.field` is required', () => { + const { field, ...entry } = getTrustedAppItemEntryItem(); + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [entry], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `entry.field` is limited to known values', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + field: '', + }, + ], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + + const bodyMsg2 = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + field: 'invalid value', + }, + ], + }; + expect(() => body.validate(bodyMsg2)).toThrow(); + + ['hash', 'path'].forEach((field) => { + const bodyMsg3 = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + field, + }, + ], + }; + + expect(() => body.validate(bodyMsg3)).not.toThrow(); + }); + }); + + it.todo('should validate `entry.type` is limited to known values'); + + it.todo('should validate `entry.operator` is limited to known values'); + + it('should validate `entry.value` required', () => { + const { value, ...entry } = getTrustedAppItemEntryItem(); + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [entry], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `entry.value` is non-empty', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + value: '', + }, + ], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + }); + }); }); 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 20fab93aaf304..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,9 +6,32 @@ 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 })), per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })), }), }; + +export const PostTrustedAppCreateRequestSchema = { + body: schema.object({ + name: schema.string({ minLength: 1 }), + description: schema.maybe(schema.string({ minLength: 1 })), + os: schema.oneOf([schema.literal('linux'), schema.literal('macos'), schema.literal('windows')]), + entries: schema.arrayOf( + schema.object({ + field: schema.oneOf([schema.literal('hash'), schema.literal('path')]), + type: schema.literal('match'), + operator: schema.literal('included'), + value: schema.string({ minLength: 1 }), + }), + { minSize: 1 } + ), + }), +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 2905274bef1cb..7aeb6c6024b99 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -5,7 +5,10 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { GetTrustedAppsRequestSchema } from '../schema/trusted_apps'; +import { + GetTrustedAppsRequestSchema, + PostTrustedAppCreateRequestSchema, +} from '../schema/trusted_apps'; /** API request params for retrieving a list of Trusted Apps */ export type GetTrustedAppsListRequest = TypeOf; @@ -16,6 +19,12 @@ export interface GetTrustedListAppsResponse { data: TrustedApp[]; } +/** API Request body for creating a new Trusted App entry */ +export type PostTrustedAppCreateRequest = TypeOf; +export interface PostTrustedAppCreateResponse { + data: TrustedApp; +} + interface MacosLinuxConditionEntry { field: 'hash' | 'path'; type: 'match'; 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..b55226b08b800 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts @@ -0,0 +1,115 @@ +/* + * 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 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 new file mode 100644 index 0000000000000..5ddcd8da30efb --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/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 { HostItem, HostsFields } from '../common'; +import { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export interface HostsEdges { + node: HostItem; + + cursor: CursorType; +} + +export interface HostsStrategyResponse extends IEsSearchResponse { + edges: HostsEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface HostsRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; +} 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 new file mode 100644 index 0000000000000..902e9909cf728 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.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 { CloudEcs } from '../../../../ecs/cloud'; +import { HostEcs, OsEcs } from '../../../../ecs/host'; +import { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../../common'; + +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + +export enum HostsFields { + lastSeen = 'lastSeen', + hostName = 'hostName', +} + +export interface EndpointFields { + endpointPolicy?: Maybe; + sensorVersion?: Maybe; + policyStatus?: Maybe; +} + +export interface HostItem { + _id?: Maybe; + cloud?: Maybe; + endpoint?: Maybe; + host?: Maybe; + lastSeen?: Maybe; +} + +export interface HostValue { + value: number; + value_as_string: string; +} + +export interface HostBucketItem { + key: string; + doc_count: number; + timestamp: HostValue; +} + +export interface HostBuckets { + buckets: HostBucketItem[]; +} + +export interface HostOsHitsItem { + hits: { + total: TotalValue | number; + max_score: number | null; + hits: Array<{ + _source: { host: { os: Maybe } }; + sort?: [number]; + _index?: string; + _type?: string; + _id?: string; + _score?: number | null; + }>; + }; +} + +export interface HostAggEsItem { + cloud_instance_id?: HostBuckets; + cloud_machine_type?: HostBuckets; + cloud_provider?: HostBuckets; + cloud_region?: HostBuckets; + firstSeen?: HostValue; + host_architecture?: HostBuckets; + host_id?: HostBuckets; + host_ip?: HostBuckets; + host_mac?: HostBuckets; + host_name?: HostBuckets; + host_os_name?: HostBuckets; + host_os_version?: HostBuckets; + host_type?: HostBuckets; + key?: string; + lastSeen?: HostValue; + os?: HostOsHitsItem; +} + +export interface HostEsData extends SearchHit { + sort: string[]; + aggregations: { + host_count: { + value: number; + }; + host_data: { + buckets: HostAggEsItem[]; + }; + }; +} + +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 new file mode 100644 index 0000000000000..adf70a109bc03 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; +import { HostsFields } from '../common'; + +export interface HostFirstLastSeenRequestOptions + extends Partial> { + hostName: string; +} +export interface HostFirstLastSeenStrategyResponse extends IEsSearchResponse { + inspect?: Maybe; + firstSeen?: Maybe; + lastSeen?: Maybe; +} 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 3a0942d2decb8..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,81 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; -import { CloudEcs } from '../../../ecs/cloud'; -import { HostEcs } from '../../../ecs/host'; - -import { - CursorType, - Inspect, - Maybe, - PageInfoPaginated, - RequestOptionsPaginated, - SortField, - TimerangeInput, -} from '..'; +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', -} - -export enum HostPolicyResponseActionStatus { - success = 'success', - failure = 'failure', - warning = 'warning', -} - -export interface EndpointFields { - endpointPolicy?: Maybe; - - sensorVersion?: Maybe; - - policyStatus?: Maybe; -} - -export interface HostItem { - _id?: Maybe; - - cloud?: Maybe; - - endpoint?: Maybe; - - host?: Maybe; - - lastSeen?: Maybe; -} - -export interface HostsEdges { - node: HostItem; - - cursor: CursorType; -} - -export interface HostsStrategyResponse extends IEsSearchResponse { - edges: HostsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface HostOverviewStrategyResponse extends IEsSearchResponse, HostItem { - inspect?: Maybe; -} - -export interface HostsRequestOptions extends RequestOptionsPaginated { - sort: SortField; - defaultIndex: string[]; -} - -export interface HostLastFirstSeenRequestOptions extends Partial { - hostName: string; -} - -export interface HostOverviewRequestOptions extends HostLastFirstSeenRequestOptions { - fields: string[]; - timerange: TimerangeInput; + 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 new file mode 100644 index 0000000000000..7d212a951905a --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe, TimerangeInput } from '../../../common'; +import { HostItem, HostsFields } from '../common'; +import { RequestOptionsPaginated } from '../..'; + +export interface HostOverviewStrategyResponse extends IEsSearchResponse { + hostOverview: HostItem; + inspect?: Maybe; +} + +export interface HostOverviewRequestOptions extends Partial> { + hostName: string; + skip?: boolean; + timerange: TimerangeInput; + inspect?: Maybe; +} 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 748cc63a3773a..873c2af53f9ac 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 @@ -8,10 +8,16 @@ 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, @@ -26,73 +32,19 @@ import { NetworkTopNFlowStrategyResponse, NetworkTopNFlowRequestOptions, } 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 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: 'lastSeen' | 'hostName'; - 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; @@ -101,26 +53,34 @@ export interface RequestBasicOptions extends IEsSearchRequest { factoryQueryType?: FactoryQueryTypes; } -export interface RequestOptions extends RequestBasicOptions { +/** A mapping of semantic fields to their document counterparts */ + +export interface RequestOptions extends RequestBasicOptions { pagination: PaginationInput; - sortField?: SortField; + sort: SortField; } -export interface RequestOptionsPaginated extends RequestBasicOptions { +export interface RequestOptionsPaginated extends RequestBasicOptions { pagination: PaginationInputPaginated; - sortField?: SortField; + sort: SortField; } export type StrategyResponseType = T extends HostsQueries.hosts ? HostsStrategyResponse : T extends HostsQueries.hostOverview ? HostOverviewStrategyResponse + : T extends HostsQueries.authentications + ? HostAuthenticationsStrategyResponse + : T extends HostsQueries.firstLastSeen + ? HostFirstLastSeenStrategyResponse + : T extends HostsQueries.uncommonProcesses + ? HostUncommonProcessesStrategyResponse : T extends NetworkQueries.dns ? NetworkDnsStrategyResponse - : T extends NetworkQueries.tls - ? NetworkTlsStrategyResponse : T extends NetworkQueries.http ? NetworkHttpStrategyResponse + : T extends NetworkQueries.tls + ? NetworkTlsStrategyResponse : T extends NetworkQueries.topCountries ? NetworkTopCountriesStrategyResponse : T extends NetworkQueries.topNFlow @@ -131,24 +91,20 @@ export type StrategyRequestType = T extends HostsQu ? HostsRequestOptions : T extends HostsQueries.hostOverview ? HostOverviewRequestOptions + : T extends HostsQueries.authentications + ? HostAuthenticationsRequestOptions + : T extends HostsQueries.firstLastSeen + ? HostFirstLastSeenRequestOptions + : T extends HostsQueries.uncommonProcesses + ? HostUncommonProcessesRequestOptions : T extends NetworkQueries.dns ? NetworkDnsRequestOptions - : T extends NetworkQueries.tls - ? NetworkTlsRequestOptions : T extends NetworkQueries.http ? NetworkHttpRequestOptions + : T extends NetworkQueries.tls + ? NetworkTlsRequestOptions : T extends NetworkQueries.topCountries ? NetworkTopCountriesRequestOptions : T extends NetworkQueries.topNFlow ? NetworkTopNFlowRequestOptions : never; - -export interface GenericBuckets { - key: string; - doc_count: number; -} - -export interface TotalValue { - value: number; - relation: string; -} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common.ts deleted file mode 100644 index 043be03972ed6..0000000000000 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common.ts +++ /dev/null @@ -1,41 +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 { GeoEcs } from '../../../ecs/geo'; -import { Maybe } from '..'; - -export enum NetworkTopTablesFields { - bytes_in = 'bytes_in', - bytes_out = 'bytes_out', - flows = 'flows', - destination_ips = 'destination_ips', - source_ips = 'source_ips', -} - -export interface NetworkTopTablesSortField { - field: NetworkTopTablesFields; - direction: Direction; -} - -export enum FlowTargetSourceDest { - destination = 'destination', - source = 'source', -} - -export enum Direction { - asc = 'asc', - desc = 'desc', -} - -export interface GeoItem { - geo?: Maybe; - flowTarget?: Maybe; -} - -export interface TopNetworkTablesEcsField { - bytes_in?: Maybe; - bytes_out?: Maybe; -} 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..66676569b3c9e --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { GeoEcs } from '../../../../ecs/geo'; +import { Maybe } from '../../..'; + +export enum NetworkTopTablesFields { + bytes_in = 'bytes_in', + bytes_out = 'bytes_out', + flows = 'flows', + destination_ips = 'destination_ips', + source_ips = 'source_ips', +} + +export enum FlowTargetSourceDest { + destination = 'destination', + source = 'source', +} + +export interface TopNetworkTablesEcsField { + bytes_in?: Maybe; + bytes_out?: Maybe; +} + +export interface GeoItem { + geo?: Maybe; + flowTarget?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns.ts deleted file mode 100644 index 09e5c09600511..0000000000000 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns.ts +++ /dev/null @@ -1,71 +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 { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; -import { Direction } from './common'; -import { CursorType, Inspect, Maybe, PageInfoPaginated, RequestOptionsPaginated } from '..'; - -export interface NetworkDnsSortField { - field: NetworkDnsFields; - - direction: Direction; -} - -export enum NetworkDnsFields { - dnsName = 'dnsName', - queryCount = 'queryCount', - uniqueDomains = 'uniqueDomains', - dnsBytesIn = 'dnsBytesIn', - dnsBytesOut = 'dnsBytesOut', -} - -export interface NetworkDnsRequestOptions extends RequestOptionsPaginated { - isPtrIncluded: boolean; - networkDnsSortField: NetworkDnsSortField; - stackByField?: Maybe; -} - -export interface NetworkDnsStrategyResponse extends IEsSearchResponse { - edges: NetworkDnsEdges[]; - totalCount: number; - pageInfo: PageInfoPaginated; - inspect?: Maybe; - histogram?: Maybe; -} - -export interface NetworkDnsEdges { - node: NetworkDnsItem; - cursor: CursorType; -} - -export interface NetworkDnsItem { - _id?: Maybe; - dnsBytesIn?: Maybe; - dnsBytesOut?: Maybe; - dnsName?: Maybe; - queryCount?: Maybe; - uniqueDomains?: Maybe; -} - -export interface MatrixOverOrdinalHistogramData { - x: string; - y: number; - g: string; -} - -export interface NetworkDnsBuckets { - key: string; - doc_count: number; - unique_domains: { - value: number; - }; - dns_bytes_in: { - value: number; - }; - dns_bytes_out: { - value: number; - }; -} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns/index.ts new file mode 100644 index 0000000000000..e3899a914ee3a --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns/index.ts @@ -0,0 +1,65 @@ +/* + * 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 { CursorType, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export enum NetworkDnsFields { + dnsName = 'dnsName', + queryCount = 'queryCount', + uniqueDomains = 'uniqueDomains', + dnsBytesIn = 'dnsBytesIn', + dnsBytesOut = 'dnsBytesOut', +} + +export interface NetworkDnsRequestOptions extends RequestOptionsPaginated { + isPtrIncluded: boolean; + sort: SortField; + stackByField?: Maybe; +} + +export interface NetworkDnsStrategyResponse extends IEsSearchResponse { + edges: NetworkDnsEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; + histogram?: Maybe; +} + +export interface NetworkDnsEdges { + node: NetworkDnsItem; + cursor: CursorType; +} + +export interface NetworkDnsItem { + _id?: Maybe; + dnsBytesIn?: Maybe; + dnsBytesOut?: Maybe; + dnsName?: Maybe; + queryCount?: Maybe; + uniqueDomains?: Maybe; +} + +export interface MatrixOverOrdinalHistogramData { + x: string; + y: number; + g: string; +} + +export interface NetworkDnsBuckets { + key: string; + doc_count: number; + unique_domains: { + value: number; + }; + dns_bytes_in: { + value: number; + }; + dns_bytes_out: { + value: number; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http.ts deleted file mode 100644 index 568444eaa36de..0000000000000 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http.ts +++ /dev/null @@ -1,71 +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 { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; -import { Direction } from './common'; -import { - CursorType, - GenericBuckets, - Inspect, - Maybe, - PageInfoPaginated, - RequestOptionsPaginated, -} from '..'; - -export interface NetworkHttpRequestOptions extends RequestOptionsPaginated { - ip?: string; - networkHttpSort: NetworkHttpSortField; - defaultIndex: string[]; -} - -export interface NetworkHttpStrategyResponse extends IEsSearchResponse { - edges: NetworkHttpEdges[]; - totalCount: number; - pageInfo: PageInfoPaginated; - inspect?: Maybe; -} - -export interface NetworkHttpSortField { - direction: Direction; -} - -export interface NetworkHttpData { - 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/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 14d7c32f46635..a0ef43eb3d899 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,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; - -import { CursorType, Inspect, Maybe, PageInfoPaginated, RequestOptionsPaginated } from '..'; - export * from './common'; export * from './dns'; export * from './http'; +export * from './tls'; export * from './top_countries'; export * from './top_n_flow'; @@ -21,91 +18,3 @@ export enum NetworkQueries { topCountries = 'topCountries', topNFlow = 'topNFlow', } - -export interface TlsBuckets { - key: string; - timestamp?: { - value: number; - value_as_string: string; - }; - - subjects: { - buckets: Readonly>; - }; - - ja3: { - buckets: Readonly>; - }; - - issuers: { - buckets: Readonly>; - }; - - not_after: { - buckets: Readonly>; - }; -} - -export interface TlsNode { - _id?: Maybe; - - timestamp?: Maybe; - - notAfter?: Maybe; - - subjects?: Maybe; - - ja3?: Maybe; - - issuers?: Maybe; -} - -export enum FlowTarget { - client = 'client', - destination = 'destination', - server = 'server', - source = 'source', -} - -export enum FlowTargetSourceDest { - destination = 'destination', - source = 'source', -} - -export enum TlsFields { - _id = '_id', -} - -export interface TlsEdges { - node: TlsNode; - - cursor: CursorType; -} - -export enum Direction { - asc = 'asc', - desc = 'desc', -} - -export interface TlsSortField { - field: TlsFields; - - direction: Direction; -} - -export interface NetworkTlsRequestOptions extends RequestOptionsPaginated { - ip: string; - flowTarget: FlowTargetSourceDest; - sort: TlsSortField; - defaultIndex: string[]; -} - -export interface NetworkTlsStrategyResponse extends IEsSearchResponse { - edges: TlsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} 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 new file mode 100644 index 0000000000000..dffc994fcf4cb --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts @@ -0,0 +1,61 @@ +/* + * 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 { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; +import { FlowTargetSourceDest } from '../common'; + +export interface TlsBuckets { + key: string; + timestamp?: { + value: number; + value_as_string: string; + }; + subjects: { + buckets: Readonly>; + }; + ja3: { + buckets: Readonly>; + }; + issuers: { + buckets: Readonly>; + }; + not_after: { + buckets: Readonly>; + }; +} + +export interface TlsNode { + _id?: Maybe; + timestamp?: Maybe; + notAfter?: Maybe; + subjects?: Maybe; + ja3?: Maybe; + issuers?: Maybe; +} + +export enum TlsFields { + _id = '_id', +} + +export interface TlsEdges { + node: TlsNode; + cursor: CursorType; +} + +export interface NetworkTlsRequestOptions extends RequestOptionsPaginated { + ip: string; + flowTarget: FlowTargetSourceDest; + defaultIndex: string[]; +} + +export interface NetworkTlsStrategyResponse extends IEsSearchResponse { + edges: TlsEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries.ts deleted file mode 100644 index 9d1895447ecff..0000000000000 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; -import { - GeoItem, - NetworkTopTablesSortField, - FlowTargetSourceDest, - TopNetworkTablesEcsField, -} from './common'; -import { CursorType, Inspect, Maybe, PageInfoPaginated, RequestOptionsPaginated } from '..'; - -export interface TopCountriesItemSource { - country?: Maybe; - destination_ips?: Maybe; - flows?: Maybe; - location?: Maybe; - source_ips?: Maybe; -} - -export interface NetworkTopCountriesRequestOptions extends RequestOptionsPaginated { - networkTopCountriesSort: NetworkTopTablesSortField; - 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 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/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..a28388a2c6f8f --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts @@ -0,0 +1,77 @@ +/* + * 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 { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; +import { + GeoItem, + FlowTargetSourceDest, + NetworkTopTablesFields, + TopNetworkTablesEcsField, +} from '../common'; + +export enum FlowTarget { + client = 'client', + destination = 'destination', + server = 'server', + source = 'source', +} + +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 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/security_solution/network/top_n_flow.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_n_flow.ts deleted file mode 100644 index 4dbf6a47ae01f..0000000000000 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_n_flow.ts +++ /dev/null @@ -1,124 +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 { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; -import { - GeoItem, - NetworkTopTablesSortField, - FlowTargetSourceDest, - TopNetworkTablesEcsField, -} from './common'; -import { - CursorType, - GenericBuckets, - Inspect, - Maybe, - PageInfoPaginated, - RequestOptionsPaginated, - TotalValue, -} from '..'; - -export interface NetworkTopNFlowRequestOptions extends RequestOptionsPaginated { - networkTopNFlowSort: NetworkTopTablesSortField; - flowTarget: FlowTargetSourceDest; - ip?: Maybe; -} - -export interface NetworkTopNFlowStrategyResponse extends IEsSearchResponse { - edges: NetworkTopNFlowEdges[]; - totalCount: number; - pageInfo: PageInfoPaginated; - inspect?: Maybe; -} - -export interface NetworkTopNFlowEdges { - node: NetworkTopNFlowItem; - cursor: CursorType; -} - -export interface NetworkTopNFlowItem { - _id?: Maybe; - source?: Maybe; - destination?: Maybe; - network?: Maybe; -} - -export interface TopNFlowItemSource { - autonomous_system?: Maybe; - domain?: Maybe; - ip?: Maybe; - location?: Maybe; - flows?: Maybe; - destination_ips?: Maybe; -} - -export interface AutonomousSystemItem { - name?: Maybe; - number?: Maybe; -} - -export interface TopNFlowItemDestination { - autonomous_system?: Maybe; - domain?: Maybe; - ip?: Maybe; - location?: Maybe; - flows?: Maybe; - source_ips?: Maybe; -} - -export interface AutonomousSystemHit { - doc_count: number; - top_as: { - hits: { - total: TotalValue | number; - max_score: number | null; - hits: Array<{ - _source: T; - sort?: [number]; - _index?: string; - _type?: string; - _id?: string; - _score?: number | null; - }>; - }; - }; -} - -export interface NetworkTopNFlowBuckets { - key: string; - autonomous_system: AutonomousSystemHit; - bytes_in: { - value: number; - }; - bytes_out: { - value: number; - }; - domain: { - buckets: GenericBuckets[]; - }; - location: LocationHit; - flows: number; - destination_ips?: number; - source_ips?: number; -} - -export interface LocationHit { - doc_count: number; - top_geo: { - hits: { - total: TotalValue | number; - max_score: number | null; - hits: Array<{ - _source: T; - sort?: [number]; - _index?: string; - _type?: string; - _id?: string; - _score?: number | null; - }>; - }; - }; -} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_n_flow/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_n_flow/index.ts new file mode 100644 index 0000000000000..d6be2d29c6eda --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_n_flow/index.ts @@ -0,0 +1,124 @@ +/* + * 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 { + GeoItem, + FlowTargetSourceDest, + TopNetworkTablesEcsField, + NetworkTopTablesFields, +} from '../common'; +import { + CursorType, + Inspect, + Maybe, + PageInfoPaginated, + TotalValue, + GenericBuckets, +} from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export interface NetworkTopNFlowRequestOptions + extends RequestOptionsPaginated { + flowTarget: FlowTargetSourceDest; + ip?: Maybe; +} + +export interface NetworkTopNFlowStrategyResponse extends IEsSearchResponse { + edges: NetworkTopNFlowEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface NetworkTopNFlowEdges { + node: NetworkTopNFlowItem; + cursor: CursorType; +} + +export interface NetworkTopNFlowItem { + _id?: Maybe; + source?: Maybe; + destination?: Maybe; + network?: Maybe; +} + +export interface TopNFlowItemSource { + autonomous_system?: Maybe; + domain?: Maybe; + ip?: Maybe; + location?: Maybe; + flows?: Maybe; + destination_ips?: Maybe; +} + +export interface AutonomousSystemItem { + name?: Maybe; + number?: Maybe; +} + +export interface TopNFlowItemDestination { + autonomous_system?: Maybe; + domain?: Maybe; + ip?: Maybe; + location?: Maybe; + flows?: Maybe; + source_ips?: Maybe; +} + +export interface AutonomousSystemHit { + doc_count: number; + top_as: { + hits: { + total: TotalValue | number; + max_score: number | null; + hits: Array<{ + _source: T; + sort?: [number]; + _index?: string; + _type?: string; + _id?: string; + _score?: number | null; + }>; + }; + }; +} + +export interface NetworkTopNFlowBuckets { + key: string; + autonomous_system: AutonomousSystemHit; + bytes_in: { + value: number; + }; + bytes_out: { + value: number; + }; + domain: { + buckets: GenericBuckets[]; + }; + location: LocationHit; + flows: number; + destination_ips?: number; + source_ips?: number; +} + +export interface LocationHit { + doc_count: number; + top_geo: { + hits: { + total: TotalValue | number; + max_score: number | null; + hits: Array<{ + _source: T; + sort?: [number]; + _index?: string; + _type?: string; + _id?: string; + _score?: number | null; + }>; + }; + }; +} 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/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index ba1de0e40e270..d9d9fde8fc8cc 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -58,6 +58,8 @@ import { createAndActivateRule, fillAboutRuleAndContinue, fillDefineCustomRuleWithImportedQueryAndContinue, + expectDefineFormToRepopulateAndContinue, + expectAboutFormToRepopulateAndContinue, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; @@ -82,6 +84,8 @@ describe('Detection rules, custom', () => { goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(newRule); fillAboutRuleAndContinue(newRule); + expectDefineFormToRepopulateAndContinue(newRule); + expectAboutFormToRepopulateAndContinue(newRule); createAndActivateRule(); cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts new file mode 100644 index 0000000000000..d8f96aaf5e563 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts @@ -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 { exportTimeline, waitForTimelinesPanelToBeLoaded } from '../tasks/timeline'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { TIMELINES_URL } from '../urls/navigation'; + +const EXPECTED_EXPORTED_TIMELINE_PATH = 'cypress/test_files/expected_timelines_export.ndjson'; + +describe('Export timelines', () => { + before(() => { + esArchiverLoad('timeline'); + cy.server(); + cy.route('POST', '**api/timeline/_export?file_name=timelines_export.ndjson*').as('export'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Exports a custom timeline', () => { + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + waitForTimelinesPanelToBeLoaded(); + + cy.readFile(EXPECTED_EXPORTED_TIMELINE_PATH).then(($expectedExportedJson) => { + const parsedJson = JSON.parse($expectedExportedJson); + const timelineId = parsedJson.savedObjectId; + exportTimeline(timelineId); + + cy.wait('@export').then((response) => { + cy.wrap(response.status).should('eql', 200); + cy.wrap(response.xhr.responseText).should('eql', $expectedExportedJson); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index 83ace877cd51d..397d0c0142179 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -6,6 +6,8 @@ export const ABOUT_CONTINUE_BTN = '[data-test-subj="about-continue"]'; +export const ABOUT_EDIT_BUTTON = '[data-test-subj="edit-about-rule"]'; + export const ADD_FALSE_POSITIVE_BTN = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text'; @@ -26,6 +28,8 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]'; export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; +export const DEFINE_EDIT_BUTTON = '[data-test-subj="edit-define-rule"]'; + export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = '[data-test-subj="importQueryFromSavedTimeline"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/search_bar.ts b/x-pack/plugins/security_solution/cypress/screens/search_bar.ts index 07e9de137826c..d2838292b04f0 100644 --- a/x-pack/plugins/security_solution/cypress/screens/search_bar.ts +++ b/x-pack/plugins/security_solution/cypress/screens/search_bar.ts @@ -14,7 +14,7 @@ export const ADD_FILTER_FORM_FIELD_INPUT = '[data-test-subj="filterFieldSuggestionList"] input[data-test-subj="comboBoxSearchInput"]'; export const ADD_FILTER_FORM_FIELD_OPTION = (value: string) => - `[data-test-subj="comboBoxOptionsList filterFieldSuggestionList-optionsList"] button[title="${value}"] strong`; + `[data-test-subj="comboBoxOptionsList filterFieldSuggestionList-optionsList"] button[title="${value}"] mark`; export const ADD_FILTER_FORM_OPERATOR_FIELD = '[data-test-subj="filterOperatorList"] input[data-test-subj="comboBoxSearchInput"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 26203a8ca3b83..fd41cd63fc090 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export const BULK_ACTIONS = '[data-test-subj="utility-bar-action-button"]'; + export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; @@ -11,6 +13,8 @@ export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; export const DRAGGABLE_HEADER = '[data-test-subj="events-viewer-panel"] [data-test-subj="headers-group"] [data-test-subj="draggable-header"]'; +export const EXPORT_TIMELINE_ACTION = '[data-test-subj="export-timeline-action"]'; + export const HEADER = '[data-test-subj="header"]'; export const HEADERS_GROUP = '[data-test-subj="headers-group"]'; @@ -41,6 +45,10 @@ export const TIMELINE = (id: string) => { export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiProgress'; +export const TIMELINE_CHECKBOX = (id: string) => { + return `[data-test-subj="checkboxSelectRow-${id}"]`; +}; + export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; @@ -70,6 +78,8 @@ export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; +export const TIMELINES_TABLE = '[data-test-subj="timelines-table"]'; + export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"]'; export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="toggle-field-@timestamp"]'; 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/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 1cce72a48e0f0..3fa300ce9d8d0 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -48,6 +48,8 @@ import { THRESHOLD_FIELD_SELECTION, THRESHOLD_INPUT_AREA, THRESHOLD_TYPE, + DEFINE_EDIT_BUTTON, + ABOUT_EDIT_BUTTON, } from '../screens/create_new_rule'; import { TIMELINE } from '../screens/timeline'; @@ -175,6 +177,20 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = ( cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; +export const expectDefineFormToRepopulateAndContinue = (rule: CustomRule) => { + cy.get(DEFINE_EDIT_BUTTON).click(); + cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); + cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); + cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist'); +}; + +export const expectAboutFormToRepopulateAndContinue = (rule: CustomRule) => { + cy.get(ABOUT_EDIT_BUTTON).click(); + cy.get(RULE_NAME_INPUT).invoke('val').should('eq', rule.name); + cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); + cy.get(ABOUT_CONTINUE_BTN).should('not.exist'); +}; + export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { const thresholdField = 0; const threshold = 1; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 08624df06e096..6fb8bb5e29ae5 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -5,8 +5,11 @@ */ import { + BULK_ACTIONS, CLOSE_TIMELINE_BTN, CREATE_NEW_TIMELINE, + EXPORT_TIMELINE_ACTION, + TIMELINE_CHECKBOX, HEADER, ID_FIELD, ID_HEADER_FIELD, @@ -20,6 +23,7 @@ import { TIMELINE_INSPECT_BUTTON, TIMELINE_SETTINGS_ICON, TIMELINE_TITLE, + TIMELINES_TABLE, TIMESTAMP_TOGGLE_FIELD, TOGGLE_TIMELINE_EXPAND_EVENT, REMOVE_COLUMN, @@ -66,6 +70,12 @@ export const expandFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true }); }; +export const exportTimeline = (timelineId: string) => { + cy.get(TIMELINE_CHECKBOX(timelineId)).click({ force: true }); + cy.get(BULK_ACTIONS).click({ force: true }); + cy.get(EXPORT_TIMELINE_ACTION).click(); +}; + export const openTimelineFieldsBrowser = () => { cy.get(TIMELINE_FIELDS_BUTTON).click({ force: true }); }; @@ -122,6 +132,10 @@ export const resetFields = () => { cy.get(RESET_FIELDS).click({ force: true }); }; +export const waitForTimelinesPanelToBeLoaded = () => { + cy.get(TIMELINES_TABLE).should('exist'); +}; + export const waitForTimelineChanges = () => { cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('exist'); cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('not.exist'); diff --git a/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson b/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson new file mode 100644 index 0000000000000..9cca356a8b052 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson @@ -0,0 +1 @@ +{"savedObjectId":"0162c130-78be-11ea-9718-118a926974a4","version":"WzcsMV0=","columns":[{"columnHeaderType":"not-filtered","id":"@timestamp"},{"columnHeaderType":"not-filtered","id":"message"},{"columnHeaderType":"not-filtered","id":"event.category"},{"columnHeaderType":"not-filtered","id":"event.action"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"source.ip"},{"columnHeaderType":"not-filtered","id":"destination.ip"},{"columnHeaderType":"not-filtered","id":"user.name"}],"created":1586256805054,"createdBy":"elastic","dataProviders":[],"dateRange":{"end":1586256837669,"start":1546343624710},"description":"description","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"host.name:*","kind":"kuery"},"serializedQuery":"{\"bool\":{\"should\":[{\"exists\":{\"field\":\"host.name\"}}],\"minimum_should_match\":1}}"}},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"title":"SIEM test","updated":1586256839298,"updatedBy":"elastic","timelineType":"default","eventNotes":[],"globalNotes":[],"pinnedEventIds":[]} 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/package.json b/x-pack/plugins/security_solution/package.json index 687099541b3d2..4d2602d1498ee 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -10,7 +10,7 @@ "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-security-solution/cypress/results > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;", "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.ts", - "test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts" + "test:generate": "node scripts/endpoint/resolver_generator" }, "devDependencies": { "@types/md5": "^2.2.0", diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index b5e952b0ffa8e..b4e9ba3dd7a71 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -15,6 +15,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { ManageUserInfo } from '../detections/components/user_info'; import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants'; import { ErrorToastDispatcher } from '../common/components/error_toast_dispatcher'; import { MlCapabilitiesProvider } from '../common/components/ml/permissions/ml_capabilities_provider'; @@ -28,6 +29,7 @@ import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; import { StartServices } from '../types'; import { PageRouter } from './routes'; import { ManageSource } from '../common/containers/sourcerer'; + interface StartAppComponent extends AppFrontendLibs { children: React.ReactNode; history: History; @@ -57,7 +59,9 @@ const StartAppComponent: FC = ({ children, apolloClient, hist - {children} + + {children} + diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 7c287646ba7ac..b48ae4e6e2d75 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -7,6 +7,7 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; +import { TimelineId } from '../../../common/types/timeline'; import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper'; import { Flyout } from '../../timelines/components/flyout'; import { HeaderGlobal } from '../../common/components/header_global'; @@ -17,6 +18,7 @@ import { useWithSource } from '../../common/containers/source'; import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; +import { useUserInfo } from '../../detections/components/user_info'; const SecuritySolutionAppWrapper = styled.div` display: flex; @@ -52,6 +54,9 @@ const HomePageComponent: React.FC = ({ children }) => { const [showTimeline] = useShowTimeline(); const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd); + // side effect: this will attempt to create the signals index if it doesn't exist + useUserInfo(); + return ( @@ -62,7 +67,7 @@ const HomePageComponent: React.FC = ({ children }) => { {indicesExist && showTimeline && ( <> - + )} diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index 841a1ef09ede6..00879ace040b9 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -5,14 +5,12 @@ */ import React, { useEffect, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { alertsDefaultModel } from './default_headers'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; @@ -68,7 +66,6 @@ const AlertsTableComponent: React.FC = ({ startDate, pageFilters = [], }) => { - const dispatch = useDispatch(); const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); const { filterManager } = useKibana().services.data.query; const { initializeTimeline } = useManageTimeline(); @@ -80,12 +77,12 @@ const AlertsTableComponent: React.FC = ({ filterManager, defaultModel: alertsDefaultModel, footerText: i18n.TOTAL_COUNT_OF_ALERTS, - timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })], title: i18n.ALERTS_TABLE_TITLE, unit: i18n.UNIT, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + return ( o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index 633135d63ac33..de9a8b32f1f90 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -15,7 +15,7 @@ import * as i18n from './translations'; import { useUiSetting$ } from '../../lib/kibana'; import { MatrixHistogramContainer } from '../matrix_histogram'; import { histogramConfigs } from './histogram_configs'; -import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; +import { MatrixHistogramConfigs } from '../matrix_histogram/types'; const ID = 'alertsOverTimeQuery'; export const AlertsView = ({ @@ -38,7 +38,7 @@ export const AlertsView = ({ [] ); const { globalFullScreen } = useFullScreen(); - const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const alertsHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, subtitle: getSubtitle, diff --git a/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx index b503d63553835..45c20b8903281 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx @@ -110,10 +110,23 @@ export const AreaChartBaseComponent = ({ position={Position.Bottom} showOverlappingTicks={false} tickFormat={xTickFormatter} - tickSize={0} + style={{ + tickLine: { + visible: false, + }, + }} /> - + ) : null; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx index cafb0095431f1..80fc1b4597081 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.tsx @@ -98,11 +98,24 @@ export const BarChartBaseComponent = ({ id={xAxisId} position={Position.Bottom} showOverlappingTicks={false} - tickSize={tickSize} + style={{ + tickLine: { + size: tickSize, + }, + }} tickFormat={xTickFormatter} /> - + ) : null; }; 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/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 6f77d15913d07..833688ae57993 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -50,10 +50,6 @@ const utilityBar = (refetch: inputsModel.Refetch, totalCount: number) => (
    ); -const exceptionsModal = (refetch: inputsModel.Refetch) => ( -
    -); - const eventsViewerDefaultProps = { browserFields: {}, columns: [], @@ -464,42 +460,4 @@ describe('EventsViewer', () => { }); }); }); - - describe('exceptions modal', () => { - test('it renders exception modal if "exceptionsModal" callback exists', async () => { - const wrapper = mount( - - - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="mock-exceptions-modal"]`).exists()).toBeTruthy(); - }); - }); - - test('it does not render exception modal if "exceptionModal" callback does not exist', async () => { - const wrapper = mount( - - - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="mock-exceptions-modal"]`).exists()).toBeFalsy(); - }); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index ebda64efabf65..3d193856a8ae4 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -109,7 +109,6 @@ interface Props { utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; // If truthy, the graph viewer (Resolver) is showing graphEventId: string | undefined; - exceptionsModal?: (refetch: inputsModel.Refetch) => React.ReactNode; } const EventsViewerComponent: React.FC = ({ @@ -135,7 +134,6 @@ const EventsViewerComponent: React.FC = ({ toggleColumn, utilityBar, graphEventId, - exceptionsModal, }) => { const { globalFullScreen } = useFullScreen(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; @@ -261,7 +259,6 @@ const EventsViewerComponent: React.FC = ({ )} - {exceptionsModal && exceptionsModal(refetch)} {utilityBar && !resolverIsShowing(graphEventId) && ( {utilityBar?.(refetch, totalCountMinusDeleted)} )} @@ -280,6 +277,7 @@ const EventsViewerComponent: React.FC = ({ docValueFields={docValueFields} id={id} isEventViewer={true} + refetch={refetch} sort={sort} toggleColumn={toggleColumn} /> @@ -338,6 +336,5 @@ export const EventsViewer = React.memo( prevProps.start === nextProps.start && prevProps.sort === nextProps.sort && prevProps.utilityBar === nextProps.utilityBar && - prevProps.graphEventId === nextProps.graphEventId && - prevProps.exceptionsModal === nextProps.exceptionsModal + prevProps.graphEventId === nextProps.graphEventId ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index ec56a3a1bd8d3..e4520dab4626a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -43,7 +43,6 @@ export interface OwnProps { headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; - exceptionsModal?: (refetch: inputsModel.Refetch) => React.ReactNode; } type Props = OwnProps & PropsFromRedux; @@ -75,7 +74,6 @@ const StatefulEventsViewerComponent: React.FC = ({ utilityBar, // If truthy, the graph viewer (Resolver) is showing graphEventId, - exceptionsModal, }) => { const [ { docValueFields, browserFields, indexPatterns, isLoading: isLoadingIndexPattern }, @@ -158,7 +156,6 @@ const StatefulEventsViewerComponent: React.FC = ({ toggleColumn={toggleColumn} utilityBar={utilityBar} graphEventId={graphEventId} - exceptionsModal={exceptionsModal} /> @@ -223,7 +220,6 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, - // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && deepEqual(prevProps.columns, nextProps.columns) && @@ -244,7 +240,6 @@ export const StatefulEventsViewer = connector( prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar && - prevProps.graphEventId === nextProps.graphEventId && - prevProps.exceptionsModal === nextProps.exceptionsModal + prevProps.graphEventId === nextProps.graphEventId ) ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 21f82c6ab4c98..c1befabdd7809 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -26,6 +26,7 @@ import { CreateExceptionListItemSchema, ExceptionListType, } from '../../../../../public/lists_plugin_deps'; +import * as i18nCommon from '../../../translations'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; @@ -49,6 +50,7 @@ import { } from '../helpers'; import { ErrorInfo, ErrorCallout } from '../error_callout'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; +import { ExceptionsBuilderExceptionItem } from '../types'; export interface AddExceptionModalBaseProps { ruleName: string; @@ -63,7 +65,7 @@ export interface AddExceptionModalBaseProps { export interface AddExceptionModalProps extends AddExceptionModalBaseProps { onCancel: () => void; - onConfirm: (didCloseAlert: boolean) => void; + onConfirm: (didCloseAlert: boolean, didBulkCloseAlert: boolean) => void; onRuleChange?: () => void; alertStatus?: Status; } @@ -117,7 +119,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ Array >([]); const [fetchOrCreateListError, setFetchOrCreateListError] = useState(null); - const { addError, addSuccess } = useAppToasts(); + const { addError, addSuccess, addWarning } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); const [ { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns }, @@ -129,16 +131,26 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ); const onError = useCallback( - (error: Error) => { + (error: Error): void => { addError(error, { title: i18n.ADD_EXCEPTION_ERROR }); onCancel(); }, [addError, onCancel] ); - const onSuccess = useCallback(() => { - addSuccess(i18n.ADD_EXCEPTION_SUCCESS); - onConfirm(shouldCloseAlert); - }, [addSuccess, onConfirm, shouldCloseAlert]); + + const onSuccess = useCallback( + (updated: number, conflicts: number): void => { + addSuccess(i18n.ADD_EXCEPTION_SUCCESS); + onConfirm(shouldCloseAlert, shouldBulkCloseAlert); + if (conflicts > 0) { + addWarning({ + title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts), + text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts), + }); + } + }, + [addSuccess, addWarning, onConfirm, shouldBulkCloseAlert, shouldCloseAlert] + ); const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException( { @@ -153,7 +165,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ exceptionItems, }: { exceptionItems: Array; - }) => { + }): void => { setExceptionItemsToAdd(exceptionItems); }, [setExceptionItemsToAdd] @@ -186,7 +198,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ); const handleFetchOrCreateExceptionListError = useCallback( - (error: Error, statusCode: number | null, message: string | null) => { + (error: Error, statusCode: number | null, message: string | null): void => { setFetchOrCreateListError({ reason: error.message, code: statusCode, @@ -205,7 +217,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ onSuccess: handleRuleChange, }); - const initialExceptionItems = useMemo(() => { + const initialExceptionItems = useMemo((): ExceptionsBuilderExceptionItem[] => { if (exceptionListType === 'endpoint' && alertData !== undefined && ruleExceptionList) { return defaultEndpointExceptionItems( exceptionListType, @@ -218,7 +230,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ } }, [alertData, exceptionListType, ruleExceptionList, ruleName]); - useEffect(() => { + useEffect((): void => { if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) { setShouldDisableBulkClose( entryHasListType(exceptionItemsToAdd) || @@ -234,34 +246,34 @@ export const AddExceptionModal = memo(function AddExceptionModal({ signalIndexPatterns, ]); - useEffect(() => { + useEffect((): void => { if (shouldDisableBulkClose === true) { setShouldBulkCloseAlert(false); } }, [shouldDisableBulkClose]); const onCommentChange = useCallback( - (value: string) => { + (value: string): void => { setComment(value); }, [setComment] ); const onCloseAlertCheckboxChange = useCallback( - (event: React.ChangeEvent) => { + (event: React.ChangeEvent): void => { setShouldCloseAlert(event.currentTarget.checked); }, [setShouldCloseAlert] ); const onBulkCloseAlertCheckboxChange = useCallback( - (event: React.ChangeEvent) => { + (event: React.ChangeEvent): void => { setShouldBulkCloseAlert(event.currentTarget.checked); }, [setShouldBulkCloseAlert] ); - const retrieveAlertOsTypes = useCallback(() => { + const retrieveAlertOsTypes = useCallback((): string[] => { const osDefaults = ['windows', 'macos']; if (alertData) { const osTypes = getMappedNonEcsValue({ @@ -276,7 +288,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({ return osDefaults; }, [alertData]); - const enrichExceptionItems = useCallback(() => { + const enrichExceptionItems = useCallback((): Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > => { let enriched: Array = []; enriched = comment !== '' @@ -289,7 +303,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ return enriched; }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]); - const onAddExceptionConfirm = useCallback(() => { + const onAddExceptionConfirm = useCallback((): void => { if (addOrUpdateExceptionItems !== null) { const alertIdToClose = shouldCloseAlert && alertData ? alertData.ecsData._id : undefined; const bulkCloseIndex = @@ -306,7 +320,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ]); const isSubmitButtonDisabled = useMemo( - () => + (): boolean => fetchOrCreateListError != null || exceptionItemsToAdd.every((item) => item.entries.length === 0), [fetchOrCreateListError, exceptionItemsToAdd] diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 46923e07d225a..2398f8d799c2c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -49,7 +49,7 @@ describe('useAddOrUpdateException', () => { const onError = jest.fn(); const onSuccess = jest.fn(); const alertIdToClose = 'idToClose'; - const bulkCloseIndex = ['.signals']; + const bulkCloseIndex = ['.custom']; const itemsToAdd: CreateExceptionListItemSchema[] = [ { ...getCreateExceptionListItemSchemaMock(), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index be289b0e85e66..dbd634e97a328 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -5,6 +5,7 @@ */ import { useEffect, useRef, useState, useCallback } from 'react'; +import { UpdateDocumentByQueryResponse } from 'elasticsearch'; import { HttpStart } from '../../../../../../../src/core/public'; import { @@ -43,7 +44,7 @@ export type ReturnUseAddOrUpdateException = [ export interface UseAddOrUpdateExceptionProps { http: HttpStart; onError: (arg: Error, code: number | null, message: string | null) => void; - onSuccess: () => void; + onSuccess: (updated: number, conficts: number) => void; } /** @@ -122,8 +123,10 @@ export const useAddOrUpdateException = ({ ) => { try { setIsLoading(true); - if (alertIdToClose !== null && alertIdToClose !== undefined) { - await updateAlertStatus({ + let alertIdResponse: UpdateDocumentByQueryResponse | undefined; + let bulkResponse: UpdateDocumentByQueryResponse | undefined; + if (alertIdToClose != null) { + alertIdResponse = await updateAlertStatus({ query: getUpdateAlertsQuery([alertIdToClose]), status: 'closed', signal: abortCtrl.signal, @@ -139,7 +142,8 @@ export const useAddOrUpdateException = ({ prepareExceptionItemsForBulkClose(exceptionItemsToAddOrUpdate), false ); - await updateAlertStatus({ + + bulkResponse = await updateAlertStatus({ query: { query: filter, }, @@ -150,9 +154,18 @@ export const useAddOrUpdateException = ({ await addOrUpdateItems(exceptionItemsToAddOrUpdate); + // NOTE: there could be some overlap here... it's possible that the first response had conflicts + // but that the alert was closed in the second call. In this case, a conflict will be reported even + // though it was already resolved. I'm not sure that there's an easy way to solve this, but it should + // have minimal impact on the user... they'd see a warning that indicates a possible conflict, but the + // state of the alerts and their representation in the UI would be consistent. + const updated = (alertIdResponse?.updated ?? 0) + (bulkResponse?.updated ?? 0); + const conflicts = + alertIdResponse?.version_conflicts ?? 0 + (bulkResponse?.version_conflicts ?? 0); + if (isSubscribed) { setIsLoading(false); - onSuccess(); + onSuccess(updated, conflicts); } } catch (error) { if (isSubscribed) { diff --git a/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx b/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx index 75d396fe384f8..19f1d70e6e230 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx @@ -6,7 +6,7 @@ import { EuiIcon, EuiLink, IconSize, IconType } from '@elastic/eui'; import { LinkAnchorProps } from '@elastic/eui/src/components/link/link'; -import React from 'react'; +import React, { ReactNode } from 'react'; import styled, { css } from 'styled-components'; interface LinkProps { @@ -47,7 +47,7 @@ export const Link = styled(({ iconSide, children, ...rest }) => ( Link.displayName = 'Link'; export interface LinkIconProps extends LinkProps { - children: string; + children: string | ReactNode; iconSize?: IconSize; iconType: IconType; dataTestSubj?: string; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index a859b0dd39231..d471b5ae9bed1 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -25,7 +25,7 @@ export interface MatrixHistogramOption { export type GetSubTitle = (count: number) => string; export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; -export interface MatrixHisrogramConfigs { +export interface MatrixHistogramConfigs { defaultStackByOption: MatrixHistogramOption; errorMessage: string; hideHistogramIfEmpty?: boolean; 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={ { - const basePath = useBasePath(); + const { getUrlForApp, navigateToApp, capabilities } = useKibana().services.application; + const canSeeAdvancedSettings = capabilities.management.kibana.settings ?? false; + const goToKibanaSettings = useCallback( + () => navigateToApp('management', { path: '/kibana/settings' }), + [navigateToApp] + ); + return ( - <> - - {i18n.NO_NEWS_MESSAGE}{' '} - - {i18n.ADVANCED_SETTINGS_LINK_TITLE} - - {'.'} - - + + {canSeeAdvancedSettings ? i18n.NO_NEWS_MESSAGE_ADMIN : i18n.NO_NEWS_MESSAGE} + {canSeeAdvancedSettings && ( + <> + {' '} + + {i18n.ADVANCED_SETTINGS_LINK_TITLE} + + {'.'} + + )} + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts b/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts index b0f9507266e52..dabaa38178884 100644 --- a/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/news_feed/translations.ts @@ -7,10 +7,17 @@ import { i18n } from '@kbn/i18n'; export const NO_NEWS_MESSAGE = i18n.translate('xpack.securitySolution.newsFeed.noNewsMessage', { - defaultMessage: - 'Your current news feed URL returned no recent news. You may update the URL or disable security news via', + defaultMessage: 'Your current news feed URL returned no recent news.', }); +export const NO_NEWS_MESSAGE_ADMIN = i18n.translate( + 'xpack.securitySolution.newsFeed.noNewsMessageForAdmin', + { + defaultMessage: + 'Your current news feed URL returned no recent news. You may update the URL or disable security news via', + } +); + export const ADVANCED_SETTINGS_LINK_TITLE = i18n.translate( 'xpack.securitySolution.newsFeed.advancedSettingsLinkTitle', { diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap index 3b8ef69892cc0..9724e36f045e4 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -24,6 +24,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta "size": "64px", }, }, + "browserDefaultFontSize": "16px", "euiAnimSlightBounce": "cubic-bezier(0.34, 1.61, 0.7, 1)", "euiAnimSlightResistance": "cubic-bezier(0.694, 0.0482, 0.335, 1)", "euiAnimSpeedExtraFast": "90ms", @@ -434,6 +435,28 @@ exports[`Paginated Table Component rendering it renders the default load more ta "euiScrollBarCorner": "6px", "euiSelectableListItemBorder": "1px solid #202128", "euiSelectableListItemPadding": "4px 12px", + "euiSelectableTemplateSitewideTypes": Object { + "application": Object { + "color": "#6092c0", + "font-weight": 500, + }, + "article": Object { + "color": "#9777bc", + "font-weight": 500, + }, + "case": Object { + "color": "#e7664c", + "font-weight": 500, + }, + "deployment": Object { + "color": "#54b399", + "font-weight": 500, + }, + "platform": Object { + "color": "#d6bf57", + "font-weight": 500, + }, + }, "euiShadowColor": "#000000", "euiShadowColorLarge": "#000000", "euiSize": "16px", diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap index b9cb5f3c83d03..f7924f37d2c17 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap @@ -26,6 +26,8 @@ exports[`Modal all errors rendering it renders the default all errors modal when data-test-subj="modal-all-errors-accordion" id="accordion1" initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} key="id-super-id-0" paddingSize="none" > diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 5e40cd00fa69e..6052913b4183b 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -12,6 +12,7 @@ import * as H from 'history'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; +import { TimelineId } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { inputsSelectors, State } from '../../store'; import { UrlInputsModel } from '../../store/inputs/model'; @@ -122,7 +123,7 @@ export const makeMapStateToProps = () => { const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; - const flyoutTimeline = getTimeline(state, 'timeline-1'); + const flyoutTimeline = getTimeline(state, TimelineId.active); const timeline = flyoutTimeline != null ? { diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts index b32919f4868dc..6a05f97da2fef 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts @@ -6,7 +6,7 @@ import * as i18n from './translations'; import { MatrixHistogramOption, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../components/matrix_histogram/types'; import { HistogramType } from '../../../../graphql/types'; @@ -19,7 +19,7 @@ export const anomaliesStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = i18n.ANOMALIES_STACK_BY_JOB_ID; -export const histogramConfigs: MatrixHisrogramConfigs = { +export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: anomaliesStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts index e0e629793952a..da43d0c510997 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts @@ -14,13 +14,16 @@ jest.mock('../lib/kibana'); describe('useDeleteList', () => { let addErrorMock: jest.Mock; let addSuccessMock: jest.Mock; + let addWarningMock: jest.Mock; beforeEach(() => { addErrorMock = jest.fn(); addSuccessMock = jest.fn(); + addWarningMock = jest.fn(); (useToasts as jest.Mock).mockImplementation(() => ({ addError: addErrorMock, addSuccess: addSuccessMock, + addWarning: addWarningMock, })); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts index bc59d87100058..ae811e7400737 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts @@ -10,7 +10,7 @@ import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/cor import { useToasts } from '../lib/kibana'; import { isAppError, AppError } from '../utils/api'; -export type UseAppToasts = Pick & { +export type UseAppToasts = Pick & { api: ToastsStart; addError: (error: unknown, options: ErrorToastOptions) => Toast; }; @@ -19,6 +19,7 @@ export const useAppToasts = (): UseAppToasts => { const toasts = useToasts(); const addError = useRef(toasts.addError.bind(toasts)).current; const addSuccess = useRef(toasts.addSuccess.bind(toasts)).current; + const addWarning = useRef(toasts.addWarning.bind(toasts)).current; const addAppError = useCallback( (error: AppError, options: ErrorToastOptions) => @@ -44,5 +45,5 @@ export const useAppToasts = (): UseAppToasts => { [addAppError, addError] ); - return { api: toasts, addError: _addError, addSuccess }; + return { api: toasts, addError: _addError, addSuccess, addWarning }; }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.ts index 075f06084384b..b4fb307a62b6c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.ts @@ -19,12 +19,11 @@ export interface WithKibanaProps { kibana: KibanaContext; } -// eslint-disable-next-line react-hooks/rules-of-hooks -const typedUseKibana = () => useKibana(); +const useTypedKibana = () => useKibana(); export { KibanaContextProvider, - typedUseKibana as useKibana, + useTypedKibana as useKibana, useUiSetting, useUiSetting$, withKibana, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index ab9f12a67fe89..26013915315af 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -5,7 +5,7 @@ */ import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter'; -import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { OpenTimelineResult } from '../../timelines/components/open_timeline/types'; import { @@ -2227,7 +2227,7 @@ export const defaultTimelineProps: CreateTimelineProps = { filters: [], highlightedDropAndProviderId: '', historyIds: [], - id: 'timeline-1', + id: TimelineId.active, isFavorite: false, isLive: false, isLoading: false, diff --git a/x-pack/plugins/security_solution/public/common/translations.ts b/x-pack/plugins/security_solution/public/common/translations.ts index 3b94ac8959496..c4a9540f62914 100644 --- a/x-pack/plugins/security_solution/public/common/translations.ts +++ b/x-pack/plugins/security_solution/public/common/translations.ts @@ -61,3 +61,17 @@ export const EMPTY_ACTION_ENDPOINT_DESCRIPTION = i18n.translate( 'Protect your hosts with threat prevention, detection, and deep security data visibility.', } ); + +export const UPDATE_ALERT_STATUS_FAILED = (conflicts: number) => + i18n.translate('xpack.securitySolution.pages.common.updateAlertStatusFailed', { + values: { conflicts }, + defaultMessage: + 'Failed to update { conflicts } {conflicts, plural, =1 {alert} other {alerts}}.', + }); + +export const UPDATE_ALERT_STATUS_FAILED_DETAILED = (updated: number, conflicts: number) => + i18n.translate('xpack.securitySolution.pages.common.updateAlertStatusFailedDetailed', { + values: { updated, conflicts }, + defaultMessage: `{ updated } {updated, plural, =1 {alert was} other {alerts were}} updated successfully, but { conflicts } failed to update + because { conflicts, plural, =1 {it was} other {they were}} already being modified.`, + }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index e8015f601cb18..3f95fd36b6010 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -18,7 +18,7 @@ import { } from '../../../common/mock/'; import { CreateTimeline, UpdateTimelineLoading } from './types'; import { Ecs } from '../../../graphql/types'; -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('apollo-client'); @@ -67,7 +67,10 @@ describe('alert actions', () => { }); expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ + id: TimelineId.active, + isLoading: true, + }); }); test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => { @@ -313,9 +316,12 @@ describe('alert actions', () => { updateTimelineIsLoading, }); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); expect(updateTimelineIsLoading).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, + isLoading: true, + }); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ + id: TimelineId.active, isLoading: false, }); expect(createTimeline).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 34c0537a6d7d2..972a8aa4b0871 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -9,7 +9,9 @@ import dateMath from '@elastic/datemath'; import { get, getOr, isEmpty, find } from 'lodash/fp'; import moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { TimelineId } from '../../../../common/types/timeline'; import { updateAlertStatus } from '../../containers/detection_engine/alerts/api'; import { SendAlertToTimelineActionProps, UpdateAlertStatusActionProps } from './types'; import { @@ -67,7 +69,6 @@ export const getFilterAndRuleBounds = ( export const updateAlertStatusAction = async ({ query, alertIds, - status, selectedStatus, setEventsLoading, setEventsDeleted, @@ -83,7 +84,18 @@ export const updateAlertStatusAction = async ({ // TODO: Only delete those that were successfully updated from updatedRules setEventsDeleted({ eventIds: alertIds, isDeleted: true }); - onAlertStatusUpdateSuccess(response.updated, selectedStatus); + if (response.version_conflicts > 0 && alertIds.length === 1) { + throw new Error( + i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.updateAlertStatusFailedSingleAlert', + { + defaultMessage: 'Failed to update alert because it was already being modified.', + } + ) + ); + } + + onAlertStatusUpdateSuccess(response.updated, response.version_conflicts, selectedStatus); } catch (error) { onAlertStatusUpdateFailure(selectedStatus, error); } finally { @@ -126,7 +138,7 @@ export const getThresholdAggregationDataProvider = ( return [ { and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-${aggregationFieldId}-${dataProviderValue}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`, name: ecsData.signal?.rule?.threshold.field, enabled: true, excluded: false, @@ -155,7 +167,7 @@ export const sendAlertToTimelineAction = async ({ if (timelineId !== '' && apolloClient != null) { try { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: true }); + updateTimelineIsLoading({ id: TimelineId.active, isLoading: true }); const [responseTimeline, eventDataResp] = await Promise.all([ apolloClient.query({ query: oneTimelineQuery, @@ -236,7 +248,7 @@ export const sendAlertToTimelineAction = async ({ } } catch { openAlertInBasicTimeline = true; - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); } } @@ -253,7 +265,7 @@ export const sendAlertToTimelineAction = async ({ dataProviders: [ { and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-${ecsData._id}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`, name: ecsData._id, enabled: true, excluded: false, @@ -266,7 +278,7 @@ export const sendAlertToTimelineAction = async ({ }, ...getThresholdAggregationDataProvider(ecsData, nonEcsData), ], - id: 'timeline-1', + id: TimelineId.active, dateRange: { start: from, end: to, @@ -304,7 +316,7 @@ export const sendAlertToTimelineAction = async ({ dataProviders: [ { and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-${ecsData._id}`, + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`, name: ecsData._id, enabled: true, excluded: false, @@ -316,7 +328,7 @@ export const sendAlertToTimelineAction = async ({ }, }, ], - id: 'timeline-1', + id: TimelineId.active, dateRange: { start: from, end: to, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index ca17d331c67e5..eebabc59d9324 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -4,44 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import ApolloClient from 'apollo-client'; -import { Dispatch } from 'redux'; - -import { EuiText } from '@elastic/eui'; -import { RuleType } from '../../../../common/detection_engine/types'; -import { isMlRule } from '../../../../common/machine_learning/helpers'; import { RowRendererId } from '../../../../common/types/timeline'; -import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; -import { - TimelineRowAction, - TimelineRowActionOnClick, -} from '../../../timelines/components/timeline/body/actions'; + import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, } from '../../../timelines/components/timeline/body/constants'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../timelines/components/timeline/helpers'; import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from './alerts_filter_group'; -import { sendAlertToTimelineAction, updateAlertStatusAction } from './actions'; import * as i18n from './translations'; -import { - CreateTimeline, - SetEventsDeletedProps, - SetEventsLoadingProps, - UpdateTimelineLoading, -} from './types'; -import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; -import { AddExceptionModalBaseProps } from '../../../common/components/exceptions/add_exception_modal'; -import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers'; -import { isThresholdRule } from '../../../../common/detection_engine/utils'; export const buildAlertStatusFilter = (status: Status): Filter[] => [ { @@ -189,13 +164,16 @@ export const alertsDefaultModel: SubsetTimelineModel = { export const requiredFieldsForActions = [ '@timestamp', + 'signal.status', 'signal.original_time', 'signal.rule.filters', 'signal.rule.from', 'signal.rule.language', 'signal.rule.query', + 'signal.rule.name', 'signal.rule.to', 'signal.rule.id', + 'signal.rule.index', 'signal.rule.type', 'signal.original_event.kind', 'signal.original_event.module', @@ -208,202 +186,3 @@ export const requiredFieldsForActions = [ 'host.os.family', 'event.code', ]; - -interface AlertActionArgs { - apolloClient?: ApolloClient<{}>; - canUserCRUD: boolean; - createTimeline: CreateTimeline; - dispatch: Dispatch; - ecsRowData: Ecs; - nonEcsRowData: TimelineNonEcsData[]; - hasIndexWrite: boolean; - onAlertStatusUpdateFailure: (status: Status, error: Error) => void; - onAlertStatusUpdateSuccess: (count: number, status: Status) => void; - setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; - setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; - status: Status; - timelineId: string; - updateTimelineIsLoading: UpdateTimelineLoading; - openAddExceptionModal: ({ - exceptionListType, - alertData, - ruleName, - ruleId, - }: AddExceptionModalBaseProps) => void; -} - -export const getAlertActions = ({ - apolloClient, - canUserCRUD, - createTimeline, - dispatch, - ecsRowData, - nonEcsRowData, - hasIndexWrite, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - timelineId, - updateTimelineIsLoading, - openAddExceptionModal, -}: AlertActionArgs): TimelineRowAction[] => { - const openAlertActionComponent: TimelineRowAction = { - ariaLabel: 'Open alert', - content: {i18n.ACTION_OPEN_ALERT}, - dataTestSubj: 'open-alert-status', - displayType: 'contextMenu', - id: FILTER_OPEN, - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, - onClick: ({ eventId }: TimelineRowActionOnClick) => - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - selectedStatus: FILTER_OPEN, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }; - - const closeAlertActionComponent: TimelineRowAction = { - ariaLabel: 'Close alert', - content: {i18n.ACTION_CLOSE_ALERT}, - dataTestSubj: 'close-alert-status', - displayType: 'contextMenu', - id: FILTER_CLOSED, - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, - onClick: ({ eventId }: TimelineRowActionOnClick) => - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - selectedStatus: FILTER_CLOSED, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }; - - const inProgressAlertActionComponent: TimelineRowAction = { - ariaLabel: 'Mark alert in progress', - content: {i18n.ACTION_IN_PROGRESS_ALERT}, - dataTestSubj: 'in-progress-alert-status', - displayType: 'contextMenu', - id: FILTER_IN_PROGRESS, - isActionDisabled: () => !canUserCRUD || !hasIndexWrite, - onClick: ({ eventId }: TimelineRowActionOnClick) => - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - status, - selectedStatus: FILTER_IN_PROGRESS, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }; - - const isEndpointAlert = () => { - const [module] = getMappedNonEcsValue({ - data: nonEcsRowData, - fieldName: 'signal.original_event.module', - }); - const [kind] = getMappedNonEcsValue({ - data: nonEcsRowData, - fieldName: 'signal.original_event.kind', - }); - return module === 'endpoint' && kind === 'alert'; - }; - - const exceptionsAreAllowed = () => { - const ruleTypes = getMappedNonEcsValue({ - data: nonEcsRowData, - fieldName: 'signal.rule.type', - }); - const [ruleType] = ruleTypes as RuleType[]; - return !isMlRule(ruleType) && !isThresholdRule(ruleType); - }; - - return [ - { - ...getInvestigateInResolverAction({ dispatch, timelineId }), - }, - { - ariaLabel: 'Send alert to timeline', - content: i18n.ACTION_INVESTIGATE_IN_TIMELINE, - dataTestSubj: 'send-alert-to-timeline', - displayType: 'icon', - iconType: 'timeline', - id: 'sendAlertToTimeline', - onClick: ({ ecsData, data }: TimelineRowActionOnClick) => - sendAlertToTimelineAction({ - apolloClient, - createTimeline, - ecsData, - nonEcsData: data, - updateTimelineIsLoading, - }), - width: DEFAULT_ICON_BUTTON_WIDTH, - }, - // Context menu items - ...(FILTER_OPEN !== status ? [openAlertActionComponent] : []), - ...(FILTER_CLOSED !== status ? [closeAlertActionComponent] : []), - ...(FILTER_IN_PROGRESS !== status ? [inProgressAlertActionComponent] : []), - { - onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { - const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); - const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); - const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); - if (ruleId !== undefined) { - openAddExceptionModal({ - ruleName: ruleName ?? '', - ruleId, - ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, - exceptionListType: 'endpoint', - alertData: { - ecsData, - nonEcsData: data, - }, - }); - } - }, - id: 'addEndpointException', - isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !isEndpointAlert(), - dataTestSubj: 'add-endpoint-exception-menu-item', - ariaLabel: 'Add Endpoint Exception', - content: {i18n.ACTION_ADD_ENDPOINT_EXCEPTION}, - displayType: 'contextMenu', - }, - { - onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { - const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); - const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); - const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); - if (ruleId !== undefined) { - openAddExceptionModal({ - ruleName: ruleName ?? '', - ruleId, - ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, - exceptionListType: 'detection', - alertData: { - ecsData, - nonEcsData: data, - }, - }); - } - }, - id: 'addException', - isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !exceptionsAreAllowed(), - dataTestSubj: 'add-exception-menu-item', - ariaLabel: 'Add Exception', - content: {i18n.ACTION_ADD_EXCEPTION}, - displayType: 'contextMenu', - }, - ]; -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index d5688d84e9759..be24957602037 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -40,8 +40,6 @@ describe('AlertsTableComponent', () => { clearEventsDeleted={jest.fn()} showBuildingBlockAlerts={false} onShowBuildingBlockAlertsChanged={jest.fn()} - updateTimelineIsLoading={jest.fn()} - updateTimeline={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 854565ace9b4b..0416b3d2a459f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -7,12 +7,12 @@ import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { connect, ConnectedProps, useDispatch } from 'react-redux'; +import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; - import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter, esQuery } from '../../../../../../../src/plugins/data/public'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HeaderSection } from '../../../common/components/header_section'; @@ -22,40 +22,29 @@ import { inputsSelectors, State, inputsModel } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { - useManageTimeline, - TimelineRowActionArgs, -} from '../../../timelines/components/manage_timeline'; -import { useApolloClient } from '../../../common/utils/apollo_context'; +import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { updateAlertStatusAction } from './actions'; import { - getAlertActions, requiredFieldsForActions, alertsDefaultModel, buildAlertStatusFilter, } from './default_config'; import { FILTER_OPEN, AlertsTableFilterGroup } from './alerts_filter_group'; import { AlertsUtilityBar } from './alerts_utility_bar'; +import * as i18nCommon from '../../../common/translations'; import * as i18n from './translations'; import { - CreateTimelineProps, SetEventsDeletedProps, SetEventsLoadingProps, UpdateAlertsStatusCallback, UpdateAlertsStatusProps, } from './types'; -import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers'; import { useStateToaster, displaySuccessToast, displayErrorToast, } from '../../../common/components/toasters'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; -import { - AddExceptionModal, - AddExceptionModalBaseProps, -} from '../../../common/components/exceptions/add_exception_modal'; interface OwnProps { timelineId: TimelineIdLiteral; @@ -72,14 +61,6 @@ interface OwnProps { type AlertsTableComponentProps = OwnProps & PropsFromRedux; -const addExceptionModalInitialState: AddExceptionModalBaseProps = { - ruleName: '', - ruleId: '', - ruleIndices: [], - exceptionListType: 'detection', - alertData: undefined, -}; - export const AlertsTableComponent: React.FC = ({ timelineId, canUserCRUD, @@ -101,30 +82,17 @@ export const AlertsTableComponent: React.FC = ({ onShowBuildingBlockAlertsChanged, signalsIndex, to, - updateTimeline, - updateTimelineIsLoading, }) => { - const dispatch = useDispatch(); - const apolloClient = useApolloClient(); - const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); - const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false); - const [addExceptionModalState, setAddExceptionModalState] = useState( - addExceptionModalInitialState - ); const [{ browserFields, indexPatterns, isLoading: indexPatternsLoading }] = useFetchIndexPatterns( signalsIndex !== '' ? [signalsIndex] : [], 'alerts_table' ); const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); - const { - initializeTimeline, - setSelectAll, - setTimelineRowActions, - setIndexToAdd, - } = useManageTimeline(); + const { addWarning } = useAppToasts(); + const { initializeTimeline, setSelectAll, setIndexToAdd } = useManageTimeline(); const getGlobalQuery = useCallback( (customFilters: Filter[]) => { @@ -149,27 +117,6 @@ export const AlertsTableComponent: React.FC = ({ [browserFields, defaultFilters, globalFilters, globalQuery, indexPatterns, kibana, to, from] ); - // Callback for creating a new timeline -- utilized by row/batch actions - const createTimelineCallback = useCallback( - ({ from: fromTimeline, timeline, to: toTimeline, ruleNote, notes }: CreateTimelineProps) => { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); - updateTimeline({ - duplicate: true, - forceNotes: true, - from: fromTimeline, - id: 'timeline-1', - notes, - timeline: { - ...timeline, - show: true, - }, - to: toTimeline, - ruleNote, - })(); - }, - [updateTimeline, updateTimelineIsLoading] - ); - const setEventsLoadingCallback = useCallback( ({ eventIds, isLoading }: SetEventsLoadingProps) => { setEventsLoading!({ id: timelineId, eventIds, isLoading }); @@ -185,21 +132,29 @@ export const AlertsTableComponent: React.FC = ({ ); const onAlertStatusUpdateSuccess = useCallback( - (count: number, status: Status) => { - let title: string; - switch (status) { - case 'closed': - title = i18n.CLOSED_ALERT_SUCCESS_TOAST(count); - break; - case 'open': - title = i18n.OPENED_ALERT_SUCCESS_TOAST(count); - break; - case 'in-progress': - title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(count); + (updated: number, conflicts: number, status: Status) => { + if (conflicts > 0) { + // Partial failure + addWarning({ + title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts), + text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts), + }); + } else { + let title: string; + switch (status) { + case 'closed': + title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated); + break; + case 'open': + title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated); + break; + case 'in-progress': + title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated); + } + displaySuccessToast(title, dispatchToaster); } - displaySuccessToast(title, dispatchToaster); }, - [dispatchToaster] + [addWarning, dispatchToaster] ); const onAlertStatusUpdateFailure = useCallback( @@ -220,28 +175,6 @@ export const AlertsTableComponent: React.FC = ({ [dispatchToaster] ); - const openAddExceptionModalCallback = useCallback( - ({ - ruleName, - ruleIndices, - ruleId, - exceptionListType, - alertData, - }: AddExceptionModalBaseProps) => { - if (alertData != null) { - setShouldShowAddExceptionModal(true); - setAddExceptionModalState({ - ruleName, - ruleId, - ruleIndices, - exceptionListType, - alertData, - }); - } - }, - [setShouldShowAddExceptionModal, setAddExceptionModalState] - ); - // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar useEffect(() => { if (isSelectAllChecked) { @@ -297,7 +230,6 @@ export const AlertsTableComponent: React.FC = ({ ? getGlobalQuery(currentStatusFilter)?.filterQuery : undefined, alertIds: Object.keys(selectedEventIds), - status, selectedStatus, setEventsDeleted: setEventsDeletedCallback, setEventsLoading: setEventsLoadingCallback, @@ -352,42 +284,6 @@ export const AlertsTableComponent: React.FC = ({ ] ); - // Send to Timeline / Update Alert Status Actions for each table row - const additionalActions = useMemo( - () => ({ ecsData, nonEcsData }: TimelineRowActionArgs) => - getAlertActions({ - apolloClient, - canUserCRUD, - createTimeline: createTimelineCallback, - ecsRowData: ecsData, - nonEcsRowData: nonEcsData, - dispatch, - hasIndexWrite, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted: setEventsDeletedCallback, - setEventsLoading: setEventsLoadingCallback, - status: filterGroup, - timelineId, - updateTimelineIsLoading, - openAddExceptionModal: openAddExceptionModalCallback, - }), - [ - apolloClient, - canUserCRUD, - createTimelineCallback, - dispatch, - hasIndexWrite, - filterGroup, - setEventsLoadingCallback, - setEventsDeletedCallback, - timelineId, - updateTimelineIsLoading, - onAlertStatusUpdateSuccess, - onAlertStatusUpdateFailure, - openAddExceptionModalCallback, - ] - ); const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); const defaultFiltersMemo = useMemo(() => { if (isEmpty(defaultFilters)) { @@ -408,21 +304,12 @@ export const AlertsTableComponent: React.FC = ({ indexToAdd: defaultIndices, loadingText: i18n.LOADING_ALERTS, selectAll: false, - timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId })], + queryFields: requiredFieldsForActions, title: '', }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - setTimelineRowActions({ - id: timelineId, - queryFields: requiredFieldsForActions, - timelineRowActions: additionalActions, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [additionalActions]); - useEffect(() => { setIndexToAdd({ id: timelineId, indexToAdd: defaultIndices }); }, [timelineId, defaultIndices, setIndexToAdd]); @@ -432,53 +319,6 @@ export const AlertsTableComponent: React.FC = ({ [onFilterGroupChangedCallback] ); - const closeAddExceptionModal = useCallback(() => { - setShouldShowAddExceptionModal(false); - setAddExceptionModalState(addExceptionModalInitialState); - }, [setShouldShowAddExceptionModal, setAddExceptionModalState]); - - const onAddExceptionCancel = useCallback(() => { - closeAddExceptionModal(); - }, [closeAddExceptionModal]); - - const onAddExceptionConfirm = useCallback( - (refetch: inputsModel.Refetch) => (): void => { - refetch(); - closeAddExceptionModal(); - }, - [closeAddExceptionModal] - ); - - // Callback for creating the AddExceptionModal and allowing it - // access to the refetchQuery to update the page - const exceptionModalCallback = useCallback( - (refetchQuery: inputsModel.Refetch) => { - if (shouldShowAddExceptionModal) { - return ( - - ); - } else { - return <>; - } - }, - [ - addExceptionModalState, - filterGroup, - onAddExceptionCancel, - onAddExceptionConfirm, - shouldShowAddExceptionModal, - ] - ); - if (loading || indexPatternsLoading || isEmpty(signalsIndex)) { return ( @@ -489,19 +329,16 @@ export const AlertsTableComponent: React.FC = ({ } return ( - <> - - + ); }; @@ -551,9 +388,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), clearEventsDeleted: ({ id }: { id: string }) => dispatch(timelineActions.clearEventsDeleted({ id })), - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(timelineActions.updateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), }); const connector = connect(makeMapStateToProps, mapDispatchToProps); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx new file mode 100644 index 0000000000000..cbf0e08fef5cd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -0,0 +1,495 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { + EuiText, + EuiButtonIcon, + EuiContextMenuPanel, + EuiPopover, + EuiContextMenuItem, +} from '@elastic/eui'; +import styled from 'styled-components'; + +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { TimelineId } from '../../../../../common/types/timeline'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; +import { Status, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { isThresholdRule } from '../../../../../common/detection_engine/utils'; +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { timelineActions } from '../../../../timelines/store/timeline'; +import { EventsTd, EventsTdContent } from '../../../../timelines/components/timeline/styles'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; +import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter_group'; +import { updateAlertStatusAction } from '../actions'; +import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; +import { Ecs, TimelineNonEcsData } from '../../../../graphql/types'; +import { + AddExceptionModal as AddExceptionModalComponent, + AddExceptionModalBaseProps, +} from '../../../../common/components/exceptions/add_exception_modal'; +import { getMappedNonEcsValue } from '../../../../common/components/exceptions/helpers'; +import * as i18nCommon from '../../../../common/translations'; +import * as i18n from '../translations'; +import { + useStateToaster, + displaySuccessToast, + displayErrorToast, +} from '../../../../common/components/toasters'; +import { inputsModel } from '../../../../common/store'; +import { useUserData } from '../../user_info'; + +interface AlertContextMenuProps { + disabled: boolean; + ecsRowData: Ecs; + nonEcsRowData: TimelineNonEcsData[]; + refetch: inputsModel.Refetch; + timelineId: string; +} + +const addExceptionModalInitialState: AddExceptionModalBaseProps = { + ruleName: '', + ruleId: '', + ruleIndices: [], + exceptionListType: 'detection', + alertData: undefined, +}; + +const AlertContextMenuComponent: React.FC = ({ + disabled, + ecsRowData, + nonEcsRowData, + refetch, + timelineId, +}) => { + const dispatch = useDispatch(); + const [, dispatchToaster] = useStateToaster(); + const [isPopoverOpen, setPopover] = useState(false); + const [alertStatus, setAlertStatus] = useState( + (ecsRowData.signal?.status && (ecsRowData.signal.status[0] as Status)) ?? undefined + ); + const eventId = ecsRowData._id; + + const { addWarning } = useAppToasts(); + + const onButtonClick = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen]); + + const closePopover = useCallback(() => { + setPopover(false); + }, []); + const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false); + const [addExceptionModalState, setAddExceptionModalState] = useState( + addExceptionModalInitialState + ); + const [{ canUserCRUD, hasIndexWrite }] = useUserData(); + + const isEndpointAlert = useMemo(() => { + if (!nonEcsRowData) { + return false; + } + + const [module] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.original_event.module', + }); + const [kind] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.original_event.kind', + }); + return module === 'endpoint' && kind === 'alert'; + }, [nonEcsRowData]); + + const closeAddExceptionModal = useCallback(() => { + setShouldShowAddExceptionModal(false); + setAddExceptionModalState(addExceptionModalInitialState); + }, [setShouldShowAddExceptionModal, setAddExceptionModalState]); + + const onAddExceptionCancel = useCallback(() => { + closeAddExceptionModal(); + }, [closeAddExceptionModal]); + + const onAddExceptionConfirm = useCallback( + (didCloseAlert: boolean, didBulkCloseAlert) => { + closeAddExceptionModal(); + if (didCloseAlert) { + setAlertStatus('closed'); + } + if (timelineId !== TimelineId.active || didBulkCloseAlert) { + refetch(); + } + }, + [closeAddExceptionModal, timelineId, refetch] + ); + + const onAlertStatusUpdateSuccess = useCallback( + (updated: number, conflicts: number, newStatus: Status) => { + if (conflicts > 0) { + // Partial failure + addWarning({ + title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts), + text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts), + }); + } else { + let title: string; + switch (newStatus) { + case 'closed': + title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated); + break; + case 'open': + title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated); + break; + case 'in-progress': + title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated); + } + displaySuccessToast(title, dispatchToaster); + } + setAlertStatus(newStatus); + }, + [dispatchToaster, addWarning] + ); + + const onAlertStatusUpdateFailure = useCallback( + (newStatus: Status, error: Error) => { + let title: string; + switch (newStatus) { + case 'closed': + title = i18n.CLOSED_ALERT_FAILED_TOAST; + break; + case 'open': + title = i18n.OPENED_ALERT_FAILED_TOAST; + break; + case 'in-progress': + title = i18n.IN_PROGRESS_ALERT_FAILED_TOAST; + } + displayErrorToast(title, [error.message], dispatchToaster); + }, + [dispatchToaster] + ); + + const setEventsLoading = useCallback( + ({ eventIds, isLoading }: SetEventsLoadingProps) => { + dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading })); + }, + [dispatch, timelineId] + ); + + const setEventsDeleted = useCallback( + ({ eventIds, isDeleted }: SetEventsDeletedProps) => { + dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted })); + }, + [dispatch, timelineId] + ); + + const openAlertActionOnClick = useCallback(() => { + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + selectedStatus: FILTER_OPEN, + }); + closePopover(); + }, [ + closePopover, + eventId, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + ]); + + const openAlertActionComponent = ( + + {i18n.ACTION_OPEN_ALERT} + + ); + + const closeAlertActionClick = useCallback(() => { + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + selectedStatus: FILTER_CLOSED, + }); + closePopover(); + }, [ + closePopover, + eventId, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + ]); + + const closeAlertActionComponent = ( + + {i18n.ACTION_CLOSE_ALERT} + + ); + + const inProgressAlertActionClick = useCallback(() => { + updateAlertStatusAction({ + alertIds: [eventId], + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + selectedStatus: FILTER_IN_PROGRESS, + }); + closePopover(); + }, [ + closePopover, + eventId, + onAlertStatusUpdateFailure, + onAlertStatusUpdateSuccess, + setEventsDeleted, + setEventsLoading, + ]); + + const inProgressAlertActionComponent = ( + + {i18n.ACTION_IN_PROGRESS_ALERT} + + ); + + const openAddExceptionModal = useCallback( + ({ + ruleName, + ruleIndices, + ruleId, + exceptionListType, + alertData, + }: AddExceptionModalBaseProps) => { + if (alertData !== null && alertData !== undefined) { + setShouldShowAddExceptionModal(true); + setAddExceptionModalState({ + ruleName, + ruleId, + ruleIndices, + exceptionListType, + alertData, + }); + } + }, + [setShouldShowAddExceptionModal, setAddExceptionModalState] + ); + + const AddExceptionModal = useCallback( + () => + shouldShowAddExceptionModal === true && addExceptionModalState.alertData !== null ? ( + + ) : null, + [ + shouldShowAddExceptionModal, + addExceptionModalState.alertData, + addExceptionModalState.ruleName, + addExceptionModalState.ruleId, + addExceptionModalState.ruleIndices, + addExceptionModalState.exceptionListType, + onAddExceptionCancel, + onAddExceptionConfirm, + alertStatus, + ] + ); + + const button = ( + + ); + + const handleAddEndpointExceptionClick = useCallback(() => { + const [ruleName] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.name', + }); + const [ruleId] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.id', + }); + const ruleIndices = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.index', + }); + + closePopover(); + + if (ruleId !== undefined) { + openAddExceptionModal({ + ruleName: ruleName ?? '', + ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, + exceptionListType: 'endpoint', + alertData: { + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + }, + }); + } + }, [closePopover, ecsRowData, nonEcsRowData, openAddExceptionModal]); + + const addEndpointExceptionComponent = ( + + {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} + + ); + + const handleAddExceptionClick = useCallback(() => { + const [ruleName] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.name', + }); + const [ruleId] = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.id', + }); + const ruleIndices = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.index', + }); + + closePopover(); + + if (ruleId !== undefined) { + openAddExceptionModal({ + ruleName: ruleName ?? '', + ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, + exceptionListType: 'detection', + alertData: { + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + }, + }); + } + }, [closePopover, ecsRowData, nonEcsRowData, openAddExceptionModal]); + + const areExceptionsAllowed = useMemo(() => { + const ruleTypes = getMappedNonEcsValue({ + data: nonEcsRowData, + fieldName: 'signal.rule.type', + }); + const [ruleType] = ruleTypes as Type[]; + return !isMlRule(ruleType) && !isThresholdRule(ruleType); + }, [nonEcsRowData]); + + const addExceptionComponent = ( + + {i18n.ACTION_ADD_EXCEPTION} + + ); + + const statusFilters = useMemo(() => { + if (!alertStatus) { + return []; + } + + switch (alertStatus) { + case 'open': + return [inProgressAlertActionComponent, closeAlertActionComponent]; + case 'in-progress': + return [openAlertActionComponent, closeAlertActionComponent]; + case 'closed': + return [openAlertActionComponent, inProgressAlertActionComponent]; + default: + return []; + } + }, [ + alertStatus, + closeAlertActionComponent, + inProgressAlertActionComponent, + openAlertActionComponent, + ]); + + const items = useMemo( + () => [...statusFilters, addEndpointExceptionComponent, addExceptionComponent], + [addEndpointExceptionComponent, addExceptionComponent, statusFilters] + ); + + return ( + <> + + + + + + + + + + ); +}; + +const ContextMenuPanel = styled(EuiContextMenuPanel)` + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; +`; + +ContextMenuPanel.displayName = 'ContextMenuPanel'; + +export const AlertContextMenu = React.memo(AlertContextMenuComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx new file mode 100644 index 0000000000000..f4080de5b4ba1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -0,0 +1,85 @@ +/* + * 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, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; + +import { TimelineId } from '../../../../../common/types/timeline'; +import { TimelineNonEcsData, Ecs } from '../../../../../public/graphql/types'; +import { timelineActions } from '../../../../timelines/store/timeline'; +import { useApolloClient } from '../../../../common/utils/apollo_context'; +import { sendAlertToTimelineAction } from '../actions'; +import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers'; +import { ActionIconItem } from '../../../../timelines/components/timeline/body/actions/action_icon_item'; + +import { CreateTimelineProps } from '../types'; +import { + ACTION_INVESTIGATE_IN_TIMELINE, + ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL, +} from '../translations'; + +interface InvestigateInTimelineActionProps { + ecsRowData: Ecs; + nonEcsRowData: TimelineNonEcsData[]; +} + +const InvestigateInTimelineActionComponent: React.FC = ({ + ecsRowData, + nonEcsRowData, +}) => { + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); + + const updateTimelineIsLoading = useCallback( + (payload) => dispatch(timelineActions.updateIsLoading(payload)), + [dispatch] + ); + + const createTimeline = useCallback( + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); + dispatchUpdateTimeline(dispatch)({ + duplicate: true, + from: fromTimeline, + id: TimelineId.active, + notes: [], + timeline: { + ...timeline, + show: true, + }, + to: toTimeline, + ruleNote, + })(); + }, + [dispatch, updateTimelineIsLoading] + ); + + const investigateInTimelineAlertClick = useCallback( + () => + sendAlertToTimelineAction({ + apolloClient, + createTimeline, + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + updateTimelineIsLoading, + }), + [apolloClient, createTimeline, ecsRowData, nonEcsRowData, updateTimelineIsLoading] + ); + + return ( + + ); +}; + +export const InvestigateInTimelineAction = React.memo(InvestigateInTimelineActionComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 3d6c3dc0a7a8e..b4da0267d2ea5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -115,6 +115,13 @@ export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( } ); +export const ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineAriaLabel', + { + defaultMessage: 'Send alert to timeline', + } +); + export const ACTION_ADD_EXCEPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.addException', { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index 2e77e77f6b3d5..f8b3cd6af8b8a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -41,11 +41,10 @@ export type UpdateAlertsStatus = ({ export interface UpdateAlertStatusActionProps { query?: string; alertIds: string[]; - status: Status; selectedStatus: Status; setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; - onAlertStatusUpdateSuccess: (count: number, status: Status) => void; + onAlertStatusUpdateSuccess: (updated: number, conflicts: number, status: Status) => void; onAlertStatusUpdateFailure: (status: Status, error: Error) => void; } 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.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index 8b3d05ce5a574..8179e5865e4ef 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; import { - StepRuleDescriptionComponent, + StepRuleDescription, addFilterStateIfNotThere, buildListItems, getDescriptionItem, @@ -52,24 +52,24 @@ describe('description_step', () => { mockAboutStep = mockAboutStepRule(); }); - describe('StepRuleDescriptionComponent', () => { + describe('StepRuleDescription', () => { test('renders tow columns when "columns" is "multi"', () => { const wrapper = shallow( - + ); expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); }); test('renders single column when "columns" is "single"', () => { const wrapper = shallow( - + ); expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); }); test('renders one column with title and description split when "columns" is "singleSplit', () => { const wrapper = shallow( - + ); expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); expect( @@ -299,7 +299,6 @@ describe('description_step', () => { describe('queryBar', () => { test('returns array of ListItems when queryBar exist', () => { const mockQueryBar = { - isNew: false, queryBar: { query: { query: 'user.name: root or user.name: admin', @@ -369,7 +368,6 @@ describe('description_step', () => { describe('threshold', () => { test('returns threshold description when threshold exist and field is empty', () => { const mockThreshold = { - isNew: false, threshold: { field: [''], value: 100, @@ -391,7 +389,6 @@ describe('description_step', () => { test('returns threshold description when threshold exist and field is set', () => { const mockThreshold = { - isNew: false, threshold: { field: ['user.name'], value: 100, 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..99e36669f78bb 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, @@ -38,10 +37,10 @@ import { buildRuleTypeDescription, buildThresholdDescription, } from './helpers'; -import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; 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 { @@ -52,22 +51,21 @@ const DescriptionListContainer = styled(EuiDescriptionList)` } `; -interface StepRuleDescriptionProps { +interface StepRuleDescriptionProps { columns?: 'multi' | 'single' | 'singleSplit'; data: unknown; indexPatterns?: IIndexPattern; - schema: FormSchema; + schema: FormSchema; } -export const StepRuleDescriptionComponent: React.FC = ({ +export const StepRuleDescriptionComponent = ({ data, columns = 'multi', indexPatterns, schema, -}) => { +}: StepRuleDescriptionProps) => { const kibana = useKibana(); const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const { jobs } = useSecurityJobs(false); const keys = Object.keys(schema); const listItems = keys.reduce((acc: ListItems[], key: string) => { @@ -76,8 +74,7 @@ export const StepRuleDescriptionComponent: React.FC = ...acc, buildMlJobDescription( get(key, data) as string, - (get(key, schema) as { label: string }).label, - jobs + (get(key, schema) as { label: string }).label ), ]; } @@ -125,11 +122,13 @@ export const StepRuleDescriptionComponent: React.FC = ); }; -export const StepRuleDescription = memo(StepRuleDescriptionComponent); +export const StepRuleDescription = memo( + StepRuleDescriptionComponent +) as typeof StepRuleDescriptionComponent; -export const buildListItems = ( +export const buildListItems = ( data: unknown, - schema: FormSchema, + schema: FormSchema, filterManager: FilterManager, indexPatterns?: IIndexPattern ): ListItems[] => @@ -211,7 +210,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/description_step/ml_job_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx index 3152fef12c652..ec46610286b46 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx @@ -14,7 +14,7 @@ jest.mock('../../../../common/lib/kibana'); describe('MlJobDescription', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx index 6baa2abab33d1..414f6f2c2d3bb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx @@ -10,6 +10,7 @@ import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { MlSummaryJob } from '../../../../../../ml/public'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { useKibana } from '../../../../common/lib/kibana'; import { ListItems } from './types'; import { ML_JOB_STARTED, ML_JOB_STOPPED } from './translations'; @@ -69,35 +70,33 @@ const Wrapper = styled.div` overflow: hidden; `; -const MlJobDescriptionComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => { +const MlJobDescriptionComponent: React.FC<{ jobId: string }> = ({ jobId }) => { + const { jobs } = useSecurityJobs(false); const jobUrl = useKibana().services.application.getUrlForApp( - `ml#/jobs?mlManagement=(jobId:${encodeURI(job.id)})` + `ml#/jobs?mlManagement=(jobId:${encodeURI(jobId)})` ); + const job = jobs.find(({ id }) => id === jobId); - return ( + const jobIdSpan = {jobId}; + + return job != null ? (
    - - {job.id} + + {jobIdSpan}
    + ) : ( + jobIdSpan ); }; export const MlJobDescription = React.memo(MlJobDescriptionComponent); -export const buildMlJobDescription = ( - jobId: string, - label: string, - jobs: MlSummaryJob[] -): ListItems => { - const job = jobs.find(({ id }) => id === jobId); - - return { - title: label, - description: job ? : jobId, - }; -}; +export const buildMlJobDescription = (jobId: string, label: string): ListItems => ({ + title: label, + description: , +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx index d97c2b4c8c0aa..7c8f5230cc8ba 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx @@ -9,7 +9,7 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elasti import * as RuleI18n from '../../../pages/detection_engine/rules/translations'; interface NextStepProps { - onClick: () => Promise; + onClick: () => void; isDisabled: boolean; dataTestSubj?: string; } 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/components/rules/step_about_rule/data.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx index 1ef3edf8c720e..08cea23d89e1e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx @@ -8,12 +8,12 @@ import styled from 'styled-components'; import { EuiHealth } from '@elastic/eui'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; -import * as I18n from './translations'; -export type SeverityValue = 'low' | 'medium' | 'high' | 'critical'; +import { Severity } from '../../../../../common/detection_engine/schemas/common/schemas'; +import * as I18n from './translations'; export interface SeverityOptionItem { - value: SeverityValue; + value: Severity; inputDisplay: React.ReactElement; } @@ -44,7 +44,7 @@ export const severityOptions: SeverityOptionItem[] = [ }, ]; -export const defaultRiskScoreBySeverity: Record = { +export const defaultRiskScoreBySeverity: Record = { low: 21, medium: 47, high: 73, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts index b9c3e4f84c18e..56c61c2ad6766 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts @@ -21,9 +21,8 @@ export const stepAboutDefaultValue: AboutStepRule = { description: '', isAssociatedToEndpointList: false, isBuildingBlock: false, - isNew: true, severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, - riskScore: { value: 50, mapping: [], isMappingChecked: false }, + riskScore: { value: 21, mapping: [], isMappingChecked: false }, references: [''], falsePositives: [''], license: '', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index 0c834b9fff33a..cb25785eaa5b2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { act } from 'react-dom/test-utils'; import { mount, shallow } from 'enzyme'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; @@ -14,9 +13,11 @@ import { StepAboutRule } from '.'; import { mockAboutStepRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; import { StepRuleDescription } from '../description_step'; import { stepAboutDefaultValue } from './default_value'; -// we don't have the types for waitFor just yet, so using "as waitFor" until when we do -import { wait as waitFor } from '@testing-library/react'; -import { AboutStepRule } from '../../../pages/detection_engine/rules/types'; +import { + AboutStepRule, + RuleStepsFormHooks, + RuleStep, +} from '../../../pages/detection_engine/rules/types'; import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers'; const theme = () => ({ eui: euiDarkVars, darkMode: true }); @@ -33,6 +34,18 @@ afterAll(() => { /* eslint-enable no-console */ describe('StepAboutRuleComponent', () => { + let formHook: RuleStepsFormHooks[RuleStep.aboutRule] | null = null; + const setFormHook = ( + step: K, + hook: RuleStepsFormHooks[K] + ) => { + formHook = hook as typeof formHook; + }; + + beforeEach(() => { + formHook = null; + }); + test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => { const wrapper = shallow( { expect(wrapper.find(StepRuleDescription).exists()).toBeTruthy(); }); - test('it prevents user from clicking continue if no "description" defined', () => { + it('is invalid if description is not present', async () => { const wrapper = mount( { defaultValues={stepAboutDefaultValue} descriptionColumns="multi" isReadOnlyView={false} + setForm={setFormHook} isLoading={false} - setForm={jest.fn()} - setStepData={jest.fn()} /> ); + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + wrapper .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') .first() .simulate('change', { target: { value: 'Test name text' } }); - const descriptionInput = wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') - .first(); - wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click'); - - expect( - wrapper.find('[data-test-subj="detectionEngineStepAboutRuleName"] input').first().props() - .value - ).toEqual('Test name text'); - expect(descriptionInput.props().value).toEqual(''); - expect( - wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] label') - .first() - .hasClass('euiFormLabel-isInvalid') - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] EuiTextArea') - .first() - .prop('isInvalid') - ).toBeTruthy(); + const result = await formHook(); + expect(result?.isValid).toEqual(false); }); - test('it prevents user from clicking continue if no "name" defined', () => { + it('is invalid if no "name" is present', async () => { const wrapper = mount( { defaultValues={stepAboutDefaultValue} descriptionColumns="multi" isReadOnlyView={false} + setForm={setFormHook} isLoading={false} - setForm={jest.fn()} - setStepData={jest.fn()} /> ); + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + wrapper .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') .first() .simulate('change', { target: { value: 'Test description text' } }); - const nameInput = wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') - .first(); - - wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click'); - - expect( - wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') - .first() - .props().value - ).toEqual('Test description text'); - expect(nameInput.props().value).toEqual(''); - expect( - wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleName"] label') - .first() - .hasClass('euiFormLabel-isInvalid') - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="detectionEngineStepAboutRuleName"] EuiFieldText') - .first() - .prop('isInvalid') - ).toBeTruthy(); + const result = await formHook(); + expect(result?.isValid).toEqual(false); }); - test('it allows user to click continue if "name" and "description" are defined', async () => { - const stepDataMock = jest.fn(); + it('is valid if both "name" and "description" are present', async () => { const wrapper = mount( { defaultValues={stepAboutDefaultValue} descriptionColumns="multi" isReadOnlyView={false} + setForm={setFormHook} isLoading={false} - setForm={jest.fn()} - setStepData={stepDataMock} /> ); + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + wrapper .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') .first() .simulate('change', { target: { value: 'Test description text' } }); - wrapper .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') .first() .simulate('change', { target: { value: 'Test name text' } }); - wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click').update(); - await waitFor(() => { - const expected: Omit = { - author: [], - isAssociatedToEndpointList: false, - isBuildingBlock: false, - license: '', - ruleNameOverride: '', - timestampOverride: '', - description: 'Test description text', - falsePositives: [''], - name: 'Test name text', - note: '', - references: [''], - riskScore: { value: 50, mapping: [], isMappingChecked: false }, - severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { id: 'none', name: 'none', reference: 'none' }, - technique: [], - }, - ], - }; - expect(stepDataMock.mock.calls[1][1]).toEqual(expected); - }); + const expected: AboutStepRule = { + author: [], + isAssociatedToEndpointList: false, + isBuildingBlock: false, + license: '', + ruleNameOverride: '', + timestampOverride: '', + description: 'Test description text', + falsePositives: [''], + name: 'Test name text', + note: '', + references: [''], + riskScore: { value: 21, mapping: [], isMappingChecked: false }, + severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, + tags: [], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + technique: [], + }, + ], + }; + + const result = await formHook(); + expect(result?.isValid).toEqual(true); + expect(result?.data).toEqual(expected); }); test('it allows user to set the risk score as a number (and not a string)', async () => { - const stepDataMock = jest.fn(); const wrapper = mount( { defaultValues={stepAboutDefaultValue} descriptionColumns="multi" isReadOnlyView={false} + setForm={setFormHook} isLoading={false} - setForm={jest.fn()} - setStepData={stepDataMock} /> ); + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + wrapper .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') .first() @@ -224,11 +203,7 @@ describe('StepAboutRuleComponent', () => { .first() .simulate('change', { target: { value: '80' } }); - await act(async () => { - wrapper.find('[data-test-subj="about-continue"]').first().simulate('click').update(); - }); - - const expected: Omit = { + const expected: AboutStepRule = { author: [], isAssociatedToEndpointList: false, isBuildingBlock: false, @@ -251,6 +226,52 @@ describe('StepAboutRuleComponent', () => { }, ], }; - expect(stepDataMock.mock.calls[1][1]).toEqual(expected); + + const result = await formHook(); + expect(result?.isValid).toEqual(true); + expect(result?.data).toEqual(expected); + }); + + it('does not modify the provided risk score until the user changes the severity', async () => { + const wrapper = mount( + + + + ); + + if (!formHook) { + throw new Error('Form hook not set, but tests depend on it'); + } + + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') + .first() + .simulate('change', { target: { value: 'Test name text' } }); + + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') + .first() + .simulate('change', { target: { value: 'Test description text' } }); + + const result = await formHook(); + expect(result?.isValid).toEqual(true); + expect(result?.data?.riskScore.value).toEqual(21); + + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleSeverity"] [data-test-subj="select"]') + .last() + .simulate('click'); + wrapper.find('button#medium').simulate('click'); + + const result2 = await formHook(); + expect(result2?.isValid).toEqual(true); + expect(result2?.data?.riskScore.value).toEqual(47); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 295b13717f076..d2c84883fa99b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -22,13 +22,14 @@ import { AddMitreThreat } from '../mitre'; import { Field, Form, - FormDataProvider, getUseField, UseField, useForm, + useFormData, + FieldHook, } from '../../../../shared_imports'; -import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; +import { defaultRiskScoreBySeverity, severityOptions } from './data'; import { stepAboutDefaultValue } from './default_value'; import { isUrlInvalid } from '../../../../common/utils/validators'; import { schema } from './schema'; @@ -68,47 +69,69 @@ const StepAboutRuleComponent: FC = ({ isReadOnlyView, isUpdateView = false, isLoading, + onSubmit, setForm, - setStepData, }) => { const initialState = defaultValues ?? stepAboutDefaultValue; - const [myStepData, setMyStepData] = useState(initialState); + const [severityValue, setSeverityValue] = useState(initialState.severity.value); const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( defineRuleData?.index ?? [], - 'step_about_rule' + RuleStep.aboutRule ); const canUseExceptions = defineRuleData?.ruleType && !isMlRule(defineRuleData.ruleType) && !isThresholdRule(defineRuleData.ruleType); - const { form } = useForm({ + const { form } = useForm({ defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); - const { getFields, submit } = form; + const { getFields, getFormData, submit } = form; + const [{ severity: formSeverity }] = (useFormData({ + form, + watch: ['severity'], + }) as unknown) as [Partial]; - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.aboutRule, null, false); - const { isValid, data } = await submit(); - if (isValid) { - setStepData(RuleStep.aboutRule, data, isValid); - setMyStepData({ ...data, isNew: false } as AboutStepRule); + useEffect(() => { + const formSeverityValue = formSeverity?.value; + if (formSeverityValue != null && formSeverityValue !== severityValue) { + setSeverityValue(formSeverityValue); + + const newRiskScoreValue = defaultRiskScoreBySeverity[formSeverityValue]; + if (newRiskScoreValue != null) { + const riskScoreField = getFields().riskScore as FieldHook; + riskScoreField.setValue({ ...riskScoreField.value, value: newRiskScoreValue }); } } - }, [setStepData, submit]); + }, [formSeverity?.value, getFields, severityValue]); + + const getData = useCallback(async () => { + const result = await submit(); + return result?.isValid + ? result + : { + isValid: false, + data: getFormData(), + }; + }, [getFormData, submit]); + + const handleSubmit = useCallback(() => { + if (onSubmit) { + onSubmit(); + } + }, [onSubmit]); useEffect(() => { if (setForm) { - setForm(RuleStep.aboutRule, form); + setForm(RuleStep.aboutRule, getData); } - }, [setForm, form]); + }, [getData, setForm]); - return isReadOnlyView && myStepData.name != null ? ( + return isReadOnlyView ? ( - + ) : ( <> @@ -305,26 +328,11 @@ const StepAboutRuleComponent: FC = ({ }} /> - - {({ severity }) => { - const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; - const severityField = getFields().severity; - const riskScoreField = getFields().riskScore; - if ( - severityField.value !== severity && - newRiskScore != null && - riskScoreField.value !== newRiskScore - ) { - riskScoreField.setValue(newRiskScore); - } - return null; - }} - {!isUpdateView && ( - + )} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index 2264a11341eb8..6df94eca429c5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -13,7 +13,7 @@ import { ValidationFunc, ERROR_CODE, } from '../../../../shared_imports'; -import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types'; +import { IMitreEnterpriseAttack, AboutStepRule } from '../../../pages/detection_engine/rules/types'; import { isMitreAttackInvalid } from '../mitre/helpers'; import { OptionalFieldLabel } from '../optional_field_label'; import { isUrlInvalid } from '../../../../common/utils/validators'; @@ -21,7 +21,7 @@ import * as I18n from './translations'; const { emptyField } = fieldValidators; -export const schema: FormSchema = { +export const schema: FormSchema = { author: { type: FIELD_TYPES.COMBO_BOX, label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 8a1f961883519..158f323b739e6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -7,15 +7,14 @@ import { EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; import React, { FC, memo, useCallback, useState, useEffect } from 'react'; import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; +import isEqual from 'lodash/isEqual'; import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; import { useUiSetting$ } from '../../../../common/lib/kibana'; import { @@ -42,9 +41,8 @@ import { getUseField, UseField, UseMultiFields, - FormDataProvider, useForm, - FormSchema, + useFormData, } from '../../../../shared_imports'; import { schema } from './schema'; import * as i18n from './translations'; @@ -52,13 +50,12 @@ import * as i18n from './translations'; const CommonUseField = getUseField({ component: Field }); interface StepDefineRuleProps extends RuleStepProps { - defaultValues?: DefineStepRule | null; + defaultValues?: DefineStepRule; } const stepDefineDefaultValue: DefineStepRule = { anomalyThreshold: 50, index: [], - isNew: true, machineLearningJobId: '', ruleType: 'query', queryBar: { @@ -103,8 +100,8 @@ const StepDefineRuleComponent: FC = ({ isReadOnlyView, isLoading, isUpdateView = false, + onSubmit, setForm, - setStepData, }) => { const mlCapabilities = useMlCapabilities(); const [openTimelineSearch, setOpenTimelineSearch] = useState(false); @@ -112,38 +109,54 @@ const StepDefineRuleComponent: FC = ({ const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); const initialState = defaultValues ?? { ...stepDefineDefaultValue, - index: indicesConfig ?? [], + index: indicesConfig, }; - const [localRuleType, setLocalRuleType] = useState(initialState.ruleType); - const [myStepData, setMyStepData] = useState(initialState); - const [ - { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, - ] = useFetchIndexPatterns(myStepData.index, 'step_define_rule'); - - const { form } = useForm({ + const { form } = useForm({ defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); - const { getFields, reset, submit } = form; - const clearErrors = useCallback(() => reset({ resetValues: false }), [reset]); + const { getFields, getFormData, reset, submit } = form; + const [{ index: formIndex, ruleType: formRuleType }] = (useFormData({ + form, + watch: ['index', 'ruleType'], + }) as unknown) as [Partial]; + const index = formIndex || initialState.index; + const ruleType = formRuleType || initialState.ruleType; + const [ + { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, + ] = useFetchIndexPatterns(index, RuleStep.defineRule); - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.defineRule, null, false); - const { isValid, data } = await submit(); - if (isValid && setStepData) { - setStepData(RuleStep.defineRule, data, isValid); - setMyStepData({ ...data, isNew: false } as DefineStepRule); - } + // reset form when rule type changes + useEffect(() => { + reset({ resetValues: false }); + }, [reset, ruleType]); + + useEffect(() => { + setIndexModified(!isEqual(index, indicesConfig)); + }, [index, indicesConfig]); + + const handleSubmit = useCallback(() => { + if (onSubmit) { + onSubmit(); } - }, [setStepData, submit]); + }, [onSubmit]); + + const getData = useCallback(async () => { + const result = await submit(); + return result?.isValid + ? result + : { + isValid: false, + data: getFormData(), + }; + }, [getFormData, submit]); useEffect(() => { if (setForm) { - setForm(RuleStep.defineRule, form); + setForm(RuleStep.defineRule, getData); } - }, [form, setForm]); + }, [getData, setForm]); const handleResetIndices = useCallback(() => { const indexField = getFields().index; @@ -173,9 +186,9 @@ const StepDefineRuleComponent: FC = ({ ) : ( @@ -192,7 +205,7 @@ const StepDefineRuleComponent: FC = ({ isMlAdmin: hasMlAdminPermissions(mlCapabilities), }} /> - + <> = ({ /> - + <> = ({ @@ -269,11 +282,9 @@ const StepDefineRuleComponent: FC = ({ fields={{ thresholdField: { path: 'threshold.field', - defaultValue: initialState.threshold.field, }, thresholdValue: { path: 'threshold.value', - defaultValue: initialState.threshold.value, }, }} > @@ -290,31 +301,11 @@ const StepDefineRuleComponent: FC = ({ dataTestSubj: 'detectionEngineStepDefineRuleTimeline', }} /> - - {({ index, ruleType }) => { - if (index != null) { - if (deepEqual(index, indicesConfig) && indexModified) { - setIndexModified(false); - } else if (!deepEqual(index, indicesConfig) && !indexModified) { - setIndexModified(true); - } - if (myStepData.index !== index) { - setMyStepData((prevValue) => ({ ...prevValue, index })); - } - } - - if (ruleType !== localRuleType) { - setLocalRuleType(ruleType); - clearErrors(); - } - return null; - }} - {!isUpdateView && ( - + )} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index 333b28bf27bbf..07eff94bbbef4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -19,9 +19,10 @@ import { FormSchema, ValidationFunc, } from '../../../../shared_imports'; +import { DefineStepRule } from '../../../pages/detection_engine/rules/types'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; -export const schema: FormSchema = { +export const schema: FormSchema = { index: { type: FIELD_TYPES.COMBO_BOX, label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 5b4f7677dbc30..e6f1c25bf9dac 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -13,7 +13,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { findIndex } from 'lodash/fp'; -import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { FC, memo, useCallback, useEffect, useMemo } from 'react'; import { ActionVariable } from '../../../../../../triggers_actions_ui/public'; import { @@ -22,7 +22,7 @@ import { ActionsStepRule, } from '../../../pages/detection_engine/rules/types'; import { StepRuleDescription } from '../description_step'; -import { Form, UseField, useForm } from '../../../../shared_imports'; +import { Form, UseField, useForm, useFormData } from '../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; import { ThrottleSelectField, @@ -40,9 +40,8 @@ interface StepRuleActionsProps extends RuleStepProps { actionMessageParams: ActionVariable[]; } -const stepActionsDefaultValue = { +const stepActionsDefaultValue: ActionsStepRule = { enabled: true, - isNew: true, actions: [], kibanaSiemAppUrl: '', throttle: DEFAULT_THROTTLE_OPTION.value, @@ -65,27 +64,16 @@ const StepRuleActionsComponent: FC = ({ isReadOnlyView, isLoading, isUpdateView = false, - setStepData, + onSubmit, setForm, actionMessageParams, }) => { - const initialState = defaultValues ?? stepActionsDefaultValue; - const [myStepData, setMyStepData] = useState(initialState); const { services: { application, triggers_actions_ui: { actionTypeRegistry }, }, } = useKibana(); - const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]); - - const { form } = useForm({ - defaultValue: initialState, - options: { stripEmptyFields: false }, - schema, - }); - const { submit } = form; - const kibanaAbsoluteUrl = useMemo( () => application.getUrlForApp(`${APP_ID}`, { @@ -93,37 +81,52 @@ const StepRuleActionsComponent: FC = ({ }), [application] ); - - const onSubmit = useCallback( - async (enabled: boolean) => { - if (setStepData) { - setStepData(RuleStep.ruleActions, null, false); - const { isValid: newIsValid, data } = await submit(); - if (newIsValid) { - setStepData(RuleStep.ruleActions, { ...data, enabled }, newIsValid); - setMyStepData({ ...data, isNew: false } as ActionsStepRule); - } + const initialState = { + ...(defaultValues ?? stepActionsDefaultValue), + kibanaSiemAppUrl: kibanaAbsoluteUrl, + }; + const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]); + const { form } = useForm({ + defaultValue: initialState, + options: { stripEmptyFields: false }, + schema, + }); + const { getFields, getFormData, submit } = form; + const [{ throttle: formThrottle }] = (useFormData({ + form, + watch: ['throttle'], + }) as unknown) as [Partial]; + const throttle = formThrottle || initialState.throttle; + + const handleSubmit = useCallback( + (enabled: boolean) => { + getFields().enabled.setValue(enabled); + if (onSubmit) { + onSubmit(); } }, - [setStepData, submit] + [getFields, onSubmit] ); + const getData = useCallback(async () => { + const result = await submit(); + return result?.isValid + ? result + : { + isValid: false, + data: getFormData(), + }; + }, [getFormData, submit]); + useEffect(() => { if (setForm) { - setForm(RuleStep.ruleActions, form); + setForm(RuleStep.ruleActions, getData); } - }, [form, setForm]); - - const updateThrottle = useCallback((throttle) => setMyStepData({ ...myStepData, throttle }), [ - myStepData, - setMyStepData, - ]); + }, [getData, setForm]); const throttleOptions = useMemo(() => { - const throttle = myStepData.throttle; - return getThrottleOptions(throttle); - }, [myStepData]); + }, [throttle]); const throttleFieldComponentProps = useMemo( () => ({ @@ -131,18 +134,16 @@ const StepRuleActionsComponent: FC = ({ isDisabled: isLoading, dataTestSubj: 'detectionEngineStepRuleActionsThrottle', hasNoInitialSelection: false, - handleChange: updateThrottle, euiFieldProps: { options: throttleOptions, }, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [isLoading, updateThrottle] + [isLoading, throttleOptions] ); - return isReadOnlyView && myStepData != null ? ( + return isReadOnlyView ? ( - + ) : ( <> @@ -154,12 +155,11 @@ const StepRuleActionsComponent: FC = ({ component={ThrottleSelectField} componentProps={throttleFieldComponentProps} /> - {myStepData.throttle !== stepActionsDefaultValue.throttle ? ( + {throttle !== stepActionsDefaultValue.throttle ? ( <> = ({ /> ) : ( - + )} - - + + @@ -197,7 +189,7 @@ const StepRuleActionsComponent: FC = ({ fill={false} isDisabled={isLoading} isLoading={isLoading} - onClick={onSubmit.bind(null, false)} + onClick={() => handleSubmit(false)} > {I18n.COMPLETE_WITHOUT_ACTIVATING}
    @@ -207,7 +199,7 @@ const StepRuleActionsComponent: FC = ({ fill isDisabled={isLoading} isLoading={isLoading} - onClick={onSubmit.bind(null, true)} + onClick={() => handleSubmit(true)} data-test-subj="create-activate" > {I18n.COMPLETE_WITH_ACTIVATING} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx index a093f991afaf7..38de3a2026eca 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx @@ -12,7 +12,8 @@ import { AlertAction, ActionTypeRegistryContract, } from '../../../../../../triggers_actions_ui/public'; -import { FormSchema, FormData, ValidationFunc, ERROR_CODE } from '../../../../shared_imports'; +import { FormSchema, ValidationFunc, ERROR_CODE } from '../../../../shared_imports'; +import { ActionsStepRule } from '../../../pages/detection_engine/rules/types'; import * as I18n from './translations'; import { isUuidv4, getActionTypeName, validateMustache, validateActionParams } from './utils'; @@ -61,7 +62,7 @@ export const getSchema = ({ actionTypeRegistry, }: { actionTypeRegistry: ActionTypeRegistryContract; -}): FormSchema => ({ +}): FormSchema => ({ actions: { validations: [ { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx index 52f04f8423bec..d451932a6b634 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, memo, useCallback, useEffect, useState } from 'react'; +import React, { FC, memo, useCallback, useEffect } from 'react'; import { RuleStep, @@ -22,9 +22,8 @@ interface StepScheduleRuleProps extends RuleStepProps { defaultValues?: ScheduleStepRule | null; } -const stepScheduleDefaultValue = { +const stepScheduleDefaultValue: ScheduleStepRule = { interval: '5m', - isNew: true, from: '1m', }; @@ -35,39 +34,44 @@ const StepScheduleRuleComponent: FC = ({ isReadOnlyView, isLoading, isUpdateView = false, - setStepData, + onSubmit, setForm, }) => { const initialState = defaultValues ?? stepScheduleDefaultValue; - const [myStepData, setMyStepData] = useState(initialState); - const { form } = useForm({ + const { form } = useForm({ defaultValue: initialState, options: { stripEmptyFields: false }, schema, }); - const { submit } = form; - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.scheduleRule, null, false); - const { isValid: newIsValid, data } = await submit(); - if (newIsValid) { - setStepData(RuleStep.scheduleRule, { ...data }, newIsValid); - setMyStepData({ ...data, isNew: false } as ScheduleStepRule); - } + const { getFormData, submit } = form; + + const handleSubmit = useCallback(() => { + if (onSubmit) { + onSubmit(); } - }, [setStepData, submit]); + }, [onSubmit]); + + const getData = useCallback(async () => { + const result = await submit(); + return result?.isValid + ? result + : { + isValid: false, + data: getFormData(), + }; + }, [getFormData, submit]); useEffect(() => { if (setForm) { - setForm(RuleStep.scheduleRule, form); + setForm(RuleStep.scheduleRule, getData); } - }, [form, setForm]); + }, [getData, setForm]); - return isReadOnlyView && myStepData != null ? ( + return isReadOnlyView ? ( - + ) : ( <> @@ -96,7 +100,7 @@ const StepScheduleRuleComponent: FC = ({ {!isUpdateView && ( - + )} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx index f4c371a2364f6..cf93a9b61710c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx @@ -9,9 +9,10 @@ import { i18n } from '@kbn/i18n'; import { OptionalFieldLabel } from '../optional_field_label'; +import { ScheduleStepRule } from '../../../pages/detection_engine/rules/types'; import { FormSchema } from '../../../../shared_imports'; -export const schema: FormSchema = { +export const schema: FormSchema = { interval: { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel', diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index 50348578cb039..e1a29c3575d95 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -121,7 +121,7 @@ export const userInfoReducer = (state: State, action: Action): State => { const StateUserInfoContext = createContext<[State, Dispatch]>([initialState, () => noop]); -const useUserData = () => useContext(StateUserInfoContext); +export const useUserData = () => useContext(StateUserInfoContext); interface ManageUserInfoProps { children: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts index 3cd819b55685c..19007c4d2e432 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts @@ -67,7 +67,7 @@ describe('Detections Alerts API', () => { }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', { body: - '{"status":"closed","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', + '{"conflicts":"proceed","status":"closed","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', method: 'POST', signal: abortCtrl.signal, }); @@ -81,7 +81,7 @@ describe('Detections Alerts API', () => { }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', { body: - '{"status":"open","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', + '{"conflicts":"proceed","status":"open","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', method: 'POST', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index 3fe676fe2c7d6..a8a2ae10a3bbd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -58,7 +58,7 @@ export const updateAlertStatus = async ({ }: UpdateAlertStatusProps): Promise => KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', - body: JSON.stringify({ status, ...query }), + body: JSON.stringify({ conflicts: 'proceed', status, ...query }), signal, }); 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/persist_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx deleted file mode 100644 index 1bf21623992e6..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { renderHook, act } from '@testing-library/react-hooks'; - -import { usePersistRule, ReturnPersistRule } from './persist_rule'; -import { ruleMock } from './mock'; - -jest.mock('./api'); - -describe('usePersistRule', () => { - test('init', async () => { - const { result } = renderHook(() => usePersistRule()); - - 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() - ); - await waitForNextUpdate(); - result.current[1](ruleMock); - 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(() => - usePersistRule() - ); - await waitForNextUpdate(); - result.current[1](ruleMock); - 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/persist_rule.tsx deleted file mode 100644 index fd139d59c0a27..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx +++ /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 { useEffect, useState, Dispatch } from 'react'; - -import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; - -import { addRule as persistRule } from './api'; -import * as i18n from './translations'; -import { NewRule } from './types'; - -interface PersistRuleReturn { - isLoading: boolean; - isSaved: boolean; -} - -export type ReturnPersistRule = [PersistRuleReturn, Dispatch]; - -export const usePersistRule = (): ReturnPersistRule => { - 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 persistRule({ 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/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/use_create_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx new file mode 100644 index 0000000000000..42d6a2a92a4c2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_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 { useCreateRule, ReturnCreateRule } from './use_create_rule'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; + +jest.mock('./api'); + +describe('useCreateRule', () => { + test('init', async () => { + 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(() => + useCreateRule() + ); + 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(() => + useCreateRule() + ); + 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_create_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx new file mode 100644 index 0000000000000..2bbd27994fc77 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_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 { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; + +import { createRule } from './api'; +import * as i18n from './translations'; + +interface CreateRuleReturn { + isLoading: boolean; + isSaved: boolean; +} + +export type ReturnCreateRule = [CreateRuleReturn, Dispatch]; + +export const useCreateRule = (): ReturnCreateRule => { + 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 createRule({ 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/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/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 982712cbe9797..8c21f6a1e8cb7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -19,7 +19,7 @@ import { } from '../../../common/mock'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { DetectionEnginePageComponent } from './detection_engine'; -import { useUserInfo } from '../../components/user_info'; +import { useUserData } from '../../components/user_info'; import { useWithSource } from '../../../common/containers/source'; import { createStore, State } from '../../../common/store'; import { mockHistory, Router } from '../../../cases/components/__mock__/router'; @@ -73,7 +73,7 @@ const store = createStore( describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index d76da592e1c81..3a3854f145db3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -30,7 +30,7 @@ import { NoApiIntegrationKeyCallOut } from '../../components/no_api_integration_ import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; -import { useUserInfo } from '../../components/user_info'; +import { useUserData } from '../../components/user_info'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; @@ -55,15 +55,17 @@ export const DetectionEnginePageComponent: React.FC = ({ }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); const { globalFullScreen } = useFullScreen(); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated: isUserAuthenticated, - hasEncryptionKey, - canUserCRUD, - signalIndexName, - hasIndexWrite, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated: isUserAuthenticated, + hasEncryptionKey, + canUserCRUD, + signalIndexName, + hasIndexWrite, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx index d4e654321ef98..045e7d402fd2b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx @@ -12,8 +12,8 @@ import { DetectionEngineContainer } from './index'; describe('DetectionEngineContainer', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find('ManageUserInfo')).toHaveLength(1); + expect(wrapper.find('Switch')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx index 914734aba4ec6..5f379f7dbb70e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx @@ -5,37 +5,32 @@ */ import React from 'react'; -import { Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; -import { ManageUserInfo } from '../../components/user_info'; import { CreateRulePage } from './rules/create'; import { DetectionEnginePage } from './detection_engine'; import { EditRulePage } from './rules/edit'; import { RuleDetailsPage } from './rules/details'; import { RulesPage } from './rules'; -type Props = Partial> & { url: string }; - -const DetectionEngineContainerComponent: React.FC = () => ( - - - - - - - - - - - - - - - - - - - +const DetectionEngineContainerComponent: React.FC = () => ( + + + + + + + + + + + + + + + + + ); export const DetectionEngineContainer = React.memo(DetectionEngineContainerComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 8c6e91254314e..5a626ce0ff00a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -165,8 +165,7 @@ export const mockRuleWithEverything = (id: string): Rule => ({ }); // TODO: update types mapping -export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ - isNew, +export const mockAboutStepRule = (): AboutStepRule => ({ author: ['Elastic'], isAssociatedToEndpointList: false, isBuildingBlock: false, @@ -200,16 +199,14 @@ export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ note: '# this is some markdown documentation', }); -export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStepRule => ({ - isNew, +export const mockActionsStepRule = (enabled = false): ActionsStepRule => ({ actions: [], kibanaSiemAppUrl: 'http://localhost:5601/app/siem', enabled, throttle: 'no_actions', }); -export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ - isNew, +export const mockDefineStepRule = (): DefineStepRule => ({ ruleType: 'query', anomalyThreshold: 50, machineLearningJobId: '', @@ -225,8 +222,7 @@ export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ }, }); -export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({ - isNew, +export const mockScheduleStepRule = (): ScheduleStepRule => ({ interval: '5m', from: '6m', to: 'now', 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..0137777f8f8f2 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 { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports'; +import { Rule } from '../../../../containers/detection_engine/rules'; +import { Type } from '../../../../../../common/detection_engine/schemas/common/schemas'; import { AboutStepRule, @@ -26,6 +26,8 @@ import { ScheduleStepRuleJson, AboutStepRuleJson, ActionsStepRuleJson, + RuleStepsFormData, + RuleStep, } from '../types'; export const getTimeTypeValue = (time: string): { unit: string; value: number } => { @@ -33,8 +35,8 @@ export const getTimeTypeValue = (time: string): { unit: string; value: number } unit: '', value: 0, }; - const filterTimeVal = (time as string).match(/\d+/g); - const filterTimeType = (time as string).match(/[a-zA-Z]+/g); + const filterTimeVal = time.match(/\d+/g); + const filterTimeType = time.match(/[a-zA-Z]+/g); if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) { timeObj.value = Number(filterTimeVal[0]); } @@ -48,6 +50,23 @@ export const getTimeTypeValue = (time: string): { unit: string; value: number } return timeObj; }; +export const stepIsValid = ( + formData?: T +): formData is { [K in keyof T]: Exclude } => + !!formData?.isValid && !!formData.data; + +export const isDefineStep = (input: unknown): input is RuleStepsFormData[RuleStep.defineRule] => + has('data.ruleType', input); + +export const isAboutStep = (input: unknown): input is RuleStepsFormData[RuleStep.aboutRule] => + has('data.name', input); + +export const isScheduleStep = (input: unknown): input is RuleStepsFormData[RuleStep.scheduleRule] => + has('data.interval', input); + +export const isActionsStep = (input: unknown): input is RuleStepsFormData[RuleStep.ruleActions] => + has('data.actions', input); + export interface RuleFields { anomalyThreshold: unknown; machineLearningJobId: unknown; @@ -68,7 +87,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 +138,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 { @@ -129,7 +148,7 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep }; export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { - const { isNew, ...formatScheduleData } = scheduleData; + const { ...formatScheduleData } = scheduleData; if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( formatScheduleData.interval @@ -161,7 +180,6 @@ export const formatAboutStepData = ( threat, isAssociatedToEndpointList, isBuildingBlock, - isNew, note, ruleNameOverride, timestampOverride, @@ -180,11 +198,11 @@ export const formatAboutStepData = ( { id: ENDPOINT_LIST_ID, list_id: ENDPOINT_LIST_ID, - namespace_type: 'agnostic', - type: 'endpoint', + namespace_type: 'agnostic' as NamespaceType, + type: 'endpoint' as ExceptionListType, }, ...detectionExceptionLists, - ] as AboutStepRuleJson['exceptions_list'], + ], } : exceptionsList != null ? { @@ -237,16 +255,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.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx index 50407c5eb219b..deffee5a56d46 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx @@ -10,7 +10,7 @@ import { shallow } from 'enzyme'; import '../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../common/mock'; import { CreateRulePage } from './index'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -29,7 +29,7 @@ jest.mock('../../../../components/user_info'); describe('CreateRulePage', () => { it('renders correctly', () => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); const wrapper = shallow(, { wrappingComponent: TestProviders }); expect(wrapper.find('[title="Create new rule"]')).toHaveLength(1); 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 70f278197b005..48247392dfe7f 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 { @@ -19,34 +20,22 @@ import { import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { AccordionTitle } from '../../../../components/rules/accordion_title'; -import { FormData, FormHook } from '../../../../../shared_imports'; -import { StepAboutRule } from '../../../../components/rules/step_about_rule'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; +import { StepAboutRule } from '../../../../components/rules/step_about_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; import { StepRuleActions } from '../../../../components/rules/step_rule_actions'; import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; import * as RuleI18n from '../translations'; import { redirectToDetections, getActionMessageParams, userHasNoPermissions } from '../helpers'; -import { - AboutStepRule, - DefineStepRule, - RuleStep, - RuleStepData, - ScheduleStepRule, - ActionsStepRule, -} from '../types'; -import { formatRule } from './helpers'; +import { RuleStep, RuleStepsFormData, RuleStepsFormHooks } from '../types'; +import { formatRule, stepIsValid } from './helpers'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../../app/types'; +import { ruleStepsOrder } from '../utils'; -const stepsRuleOrder = [ - RuleStep.defineRule, - RuleStep.aboutRule, - RuleStep.scheduleRule, - RuleStep.ruleActions, -]; +const formHookNoop = async (): Promise => undefined; const MyEuiPanel = styled(EuiPanel)<{ zindex?: number; @@ -84,108 +73,152 @@ const StepDefineRuleAccordion: StyledComponent< StepDefineRuleAccordion.displayName = 'StepDefineRuleAccordion'; const CreateRulePageComponent: React.FC = () => { - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, } = useListsConfig(); const loading = userInfoLoading || listsConfigLoading; const [, dispatchToaster] = useStateToaster(); - const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); + const [activeStep, setActiveStep] = useState(RuleStep.defineRule); + const getNextStep = (step: RuleStep): RuleStep | undefined => + ruleStepsOrder[ruleStepsOrder.indexOf(step) + 1]; const defineRuleRef = useRef(null); const aboutRuleRef = useRef(null); const scheduleRuleRef = useRef(null); const ruleActionsRef = useRef(null); - const stepsForm = useRef | null>>({ - [RuleStep.defineRule]: null, - [RuleStep.aboutRule]: null, - [RuleStep.scheduleRule]: null, - [RuleStep.ruleActions]: null, + const formHooks = useRef({ + [RuleStep.defineRule]: formHookNoop, + [RuleStep.aboutRule]: formHookNoop, + [RuleStep.scheduleRule]: formHookNoop, + [RuleStep.ruleActions]: formHookNoop, }); - const stepsData = useRef>({ + const setFormHook = useCallback( + (step: K, hook: RuleStepsFormHooks[K]) => { + formHooks.current[step] = hook; + }, + [] + ); + const stepsData = useRef({ [RuleStep.defineRule]: { isValid: false, data: undefined }, [RuleStep.aboutRule]: { isValid: false, data: undefined }, [RuleStep.scheduleRule]: { isValid: false, data: undefined }, [RuleStep.ruleActions]: { isValid: false, data: undefined }, }); - const [isStepRuleInReadOnlyView, setIsStepRuleInEditView] = useState>({ + const setStepData = ( + step: K, + data: RuleStepsFormData[K] + ): void => { + stepsData.current[step] = data; + }; + const [openSteps, setOpenSteps] = useState({ [RuleStep.defineRule]: false, [RuleStep.aboutRule]: false, [RuleStep.scheduleRule]: false, [RuleStep.ruleActions]: false, }); - const [{ isLoading, isSaved }, setRule] = usePersistRule(); - const actionMessageParams = useMemo( - () => - getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule)?.ruleType), - // eslint-disable-next-line react-hooks/exhaustive-deps - [stepsData.current['define-rule'].data] - ); + const [{ isLoading, isSaved }, setRule] = useCreateRule(); + const ruleType = stepsData.current[RuleStep.defineRule].data?.ruleType; + const ruleName = stepsData.current[RuleStep.aboutRule].data?.name; + const actionMessageParams = useMemo(() => getActionMessageParams(ruleType), [ruleType]); const history = useHistory(); - const setStepData = useCallback( - (step: RuleStep, data: unknown, isValid: boolean) => { - stepsData.current[step] = { ...stepsData.current[step], data, isValid }; - if (isValid) { - const stepRuleIdx = stepsRuleOrder.findIndex((item) => step === item); - if ([0, 1, 2].includes(stepRuleIdx)) { - if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) { - setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [step]: true, - [stepsRuleOrder[stepRuleIdx + 1]]: false, - }); - } else if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [step]: true, - }); - openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); - setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + const handleAccordionToggle = useCallback( + (step: RuleStep, isOpen: boolean) => + setOpenSteps((_openSteps) => ({ + ..._openSteps, + [step]: isOpen, + })), + [] + ); + const goToStep = useCallback( + (step: RuleStep) => { + if (ruleStepsOrder.indexOf(step) > ruleStepsOrder.indexOf(activeStep) && !openSteps[step]) { + toggleStepAccordion(step); + } + setActiveStep(step); + }, + [activeStep, openSteps] + ); + + const toggleStepAccordion = (step: RuleStep | null) => { + if (step === RuleStep.defineRule) { + defineRuleRef.current?.onToggle(); + } else if (step === RuleStep.aboutRule) { + aboutRuleRef.current?.onToggle(); + } else if (step === RuleStep.scheduleRule) { + scheduleRuleRef.current?.onToggle(); + } else if (step === RuleStep.ruleActions) { + ruleActionsRef.current?.onToggle(); + } + }; + + const editStep = useCallback( + async (step: RuleStep) => { + const activeStepData = await formHooks.current[activeStep](); + + if (activeStepData?.isValid) { + setStepData(activeStep, activeStepData); + goToStep(step); + } + }, + [activeStep, goToStep] + ); + const submitStep = useCallback( + async (step: RuleStep) => { + const stepData = await formHooks.current[step](); + + if (stepData?.isValid) { + setStepData(step, stepData); + const nextStep = getNextStep(step); + + if (nextStep != null) { + goToStep(nextStep); + } else { + const defineStep = await stepsData.current[RuleStep.defineRule]; + const aboutStep = await stepsData.current[RuleStep.aboutRule]; + const scheduleStep = await stepsData.current[RuleStep.scheduleRule]; + const actionsStep = await stepsData.current[RuleStep.ruleActions]; + + if ( + stepIsValid(defineStep) && + stepIsValid(aboutStep) && + stepIsValid(scheduleStep) && + stepIsValid(actionsStep) + ) { + setRule( + formatRule( + defineStep.data, + aboutStep.data, + scheduleStep.data, + actionsStep.data + ) + ); } - } else if ( - stepRuleIdx === 3 && - stepsData.current[RuleStep.defineRule].isValid && - stepsData.current[RuleStep.aboutRule].isValid && - stepsData.current[RuleStep.scheduleRule].isValid - ) { - setRule( - formatRule( - stepsData.current[RuleStep.defineRule].data as DefineStepRule, - stepsData.current[RuleStep.aboutRule].data as AboutStepRule, - stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule, - stepsData.current[RuleStep.ruleActions].data as ActionsStepRule - ) - ); } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [isStepRuleInReadOnlyView, openAccordionId, stepsData.current, setRule] + [goToStep, setRule] ); - const setStepsForm = useCallback((step: RuleStep, form: FormHook) => { - stepsForm.current[step] = form; - }, []); - const getAccordionType = useCallback( - (accordionId: RuleStep) => { - if (accordionId === openAccordionId) { + (step: RuleStep) => { + if (step === activeStep) { return 'active'; - } else if (stepsData.current[accordionId].isValid) { + } else if (stepsData.current[step].isValid) { return 'valid'; } return 'passive'; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [openAccordionId, stepsData.current] + [activeStep] ); const defineRuleButton = ( @@ -195,7 +228,6 @@ const CreateRulePageComponent: React.FC = () => { type={getAccordionType(RuleStep.defineRule)} /> ); - const aboutRuleButton = ( { type={getAccordionType(RuleStep.aboutRule)} /> ); - const scheduleRuleButton = ( { type={getAccordionType(RuleStep.scheduleRule)} /> ); - const ruleActionsButton = ( { /> ); - const openCloseAccordion = (accordionId: RuleStep | null) => { - if (accordionId != null) { - if (accordionId === RuleStep.defineRule && defineRuleRef.current != null) { - defineRuleRef.current.onToggle(); - } else if (accordionId === RuleStep.aboutRule && aboutRuleRef.current != null) { - aboutRuleRef.current.onToggle(); - } else if (accordionId === RuleStep.scheduleRule && scheduleRuleRef.current != null) { - scheduleRuleRef.current.onToggle(); - } else if (accordionId === RuleStep.ruleActions && ruleActionsRef.current != null) { - ruleActionsRef.current.onToggle(); - } - } - }; - - const manageAccordions = useCallback( - (id: RuleStep, isOpen: boolean) => { - const activeRuleIdx = stepsRuleOrder.findIndex((step) => step === openAccordionId); - const stepRuleIdx = stepsRuleOrder.findIndex((step) => step === id); - - if ((id === openAccordionId || stepRuleIdx < activeRuleIdx) && !isOpen) { - openCloseAccordion(id); - } else if (stepRuleIdx >= activeRuleIdx) { - if ( - openAccordionId !== id && - !stepsData.current[openAccordionId].isValid && - !isStepRuleInReadOnlyView[id] && - isOpen - ) { - openCloseAccordion(id); - } - } - }, - [isStepRuleInReadOnlyView, openAccordionId, stepsData] - ); - - const manageIsEditable = useCallback( - async (id: RuleStep) => { - const activeForm = await stepsForm.current[openAccordionId]?.submit(); - if (activeForm != null && activeForm?.isValid) { - stepsData.current[openAccordionId] = { - ...stepsData.current[openAccordionId], - data: activeForm.data, - isValid: activeForm.isValid, - }; - setOpenAccordionId(id); - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [openAccordionId]: true, - [id]: false, - }); - } - }, - [isStepRuleInReadOnlyView, openAccordionId] - ); - - if (isSaved) { - const ruleName = (stepsData.current[RuleStep.aboutRule].data as AboutStepRule).name; + if (isSaved && ruleName) { displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster); history.replace(getRulesUrl()); return null; @@ -317,13 +291,14 @@ const CreateRulePageComponent: React.FC = () => { buttonContent={defineRuleButton} paddingSize="xs" ref={defineRuleRef} - onToggle={manageAccordions.bind(null, RuleStep.defineRule)} + onToggle={handleAccordionToggle.bind(null, RuleStep.defineRule)} extraAction={ stepsData.current[RuleStep.defineRule].isValid && ( editStep(RuleStep.defineRule)} > {i18n.EDIT_RULE} @@ -333,11 +308,11 @@ const CreateRulePageComponent: React.FC = () => { submitStep(RuleStep.defineRule)} descriptionColumns="singleSplit" /> @@ -350,13 +325,14 @@ const CreateRulePageComponent: React.FC = () => { buttonContent={aboutRuleButton} paddingSize="xs" ref={aboutRuleRef} - onToggle={manageAccordions.bind(null, RuleStep.aboutRule)} + onToggle={handleAccordionToggle.bind(null, RuleStep.aboutRule)} extraAction={ stepsData.current[RuleStep.aboutRule].isValid && ( editStep(RuleStep.aboutRule)} > {i18n.EDIT_RULE} @@ -366,13 +342,13 @@ const CreateRulePageComponent: React.FC = () => { submitStep(RuleStep.aboutRule)} /> @@ -384,13 +360,13 @@ const CreateRulePageComponent: React.FC = () => { buttonContent={scheduleRuleButton} paddingSize="xs" ref={scheduleRuleRef} - onToggle={manageAccordions.bind(null, RuleStep.scheduleRule)} + onToggle={handleAccordionToggle.bind(null, RuleStep.scheduleRule)} extraAction={ stepsData.current[RuleStep.scheduleRule].isValid && ( editStep(RuleStep.scheduleRule)} > {i18n.EDIT_RULE} @@ -400,12 +376,12 @@ const CreateRulePageComponent: React.FC = () => { submitStep(RuleStep.scheduleRule)} /> @@ -417,13 +393,13 @@ const CreateRulePageComponent: React.FC = () => { buttonContent={ruleActionsButton} paddingSize="xs" ref={ruleActionsRef} - onToggle={manageAccordions.bind(null, RuleStep.ruleActions)} + onToggle={handleAccordionToggle.bind(null, RuleStep.ruleActions)} extraAction={ stepsData.current[RuleStep.ruleActions].isValid && ( editStep(RuleStep.ruleActions)} > {i18n.EDIT_RULE} @@ -433,10 +409,11 @@ const CreateRulePageComponent: React.FC = () => { submitStep(RuleStep.ruleActions)} actionMessageParams={actionMessageParams} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 5e6587dab1736..f8f9da78b2a06 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -19,7 +19,7 @@ import { import { RuleDetailsPageComponent } from './index'; import { createStore, State } from '../../../../../common/store'; import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { useWithSource } from '../../../../../common/containers/source'; import { useParams } from 'react-router-dom'; import { mockHistory, Router } from '../../../../../cases/components/__mock__/router'; @@ -69,7 +69,7 @@ const store = createStore( describe('RuleDetailsPageComponent', () => { beforeAll(() => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (useParams as jest.Mock).mockReturnValue({}); (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index f48dc64966bfc..2988e031c4dd6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -44,7 +44,7 @@ import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_ab import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; import { AlertsTable } from '../../../../components/alerts_table'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { OverviewEmpty } from '../../../../../overview/components/overview_empty'; import { useAlertInfo } from '../../../../components/alerts_info'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; @@ -124,15 +124,17 @@ export const RuleDetailsPageComponent: FC = ({ setAbsoluteRangeDatePicker, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexWrite, - signalIndexName, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexWrite, + signalIndexName, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx index 2e45dbc6521b7..e89c899b12c39 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx @@ -10,7 +10,7 @@ import { shallow } from 'enzyme'; import '../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../common/mock'; import { EditRulePage } from './index'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { useParams } from 'react-router-dom'; jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); @@ -28,7 +28,7 @@ jest.mock('react-router-dom', () => { describe('EditRulePage', () => { it('renders correctly', () => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (useParams as jest.Mock).mockReturnValue({}); const wrapper = shallow(, { wrappingComponent: TestProviders }); 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 4033d247c4ecb..5f4fd59669103 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,8 @@ 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 { UpdateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; +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 { @@ -26,15 +27,21 @@ import { } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; -import { useUserInfo } from '../../../../components/user_info'; +import { useUserData } from '../../../../components/user_info'; import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; -import { FormHook, FormData } from '../../../../../shared_imports'; import { StepPanel } from '../../../../components/rules/step_panel'; import { StepAboutRule } from '../../../../components/rules/step_about_rule'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; import { StepRuleActions } from '../../../../components/rules/step_rule_actions'; -import { formatRule } from '../create/helpers'; +import { + formatRule, + stepIsValid, + isDefineStep, + isAboutStep, + isScheduleStep, + isActionsStep, +} from '../create/helpers'; import { getStepsData, redirectToDetections, @@ -42,90 +49,72 @@ import { userHasNoPermissions, } from '../helpers'; import * as ruleI18n from '../translations'; -import { - RuleStep, - DefineStepRule, - AboutStepRule, - ScheduleStepRule, - ActionsStepRule, -} from '../types'; +import { RuleStep, RuleStepsFormHooks, RuleStepsFormData, RuleStepsData } from '../types'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../../app/types'; +import { ruleStepsOrder } from '../utils'; -interface StepRuleForm { - isValid: boolean; -} -interface AboutStepRuleForm extends StepRuleForm { - data: AboutStepRule | null; -} -interface DefineStepRuleForm extends StepRuleForm { - data: DefineStepRule | null; -} -interface ScheduleStepRuleForm extends StepRuleForm { - data: ScheduleStepRule | null; -} - -interface ActionsStepRuleForm extends StepRuleForm { - data: ActionsStepRule | null; -} +const formHookNoop = async (): Promise => undefined; const EditRulePageComponent: FC = () => { const history = useHistory(); const [, dispatchToaster] = useStateToaster(); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + }, + ] = useUserData(); const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, } = useListsConfig(); - const initLoading = userInfoLoading || listsConfigLoading; const { detailName: ruleId } = useParams<{ detailName: string | undefined }>(); - const [loading, rule] = useRule(ruleId); + const [ruleLoading, rule] = useRule(ruleId); + const loading = ruleLoading || userInfoLoading || listsConfigLoading; - const [initForm, setInitForm] = useState(false); - const [myAboutRuleForm, setMyAboutRuleForm] = useState({ - data: null, - isValid: false, - }); - const [myDefineRuleForm, setMyDefineRuleForm] = useState({ - data: null, - isValid: false, + const formHooks = useRef({ + [RuleStep.defineRule]: formHookNoop, + [RuleStep.aboutRule]: formHookNoop, + [RuleStep.scheduleRule]: formHookNoop, + [RuleStep.ruleActions]: formHookNoop, }); - const [myScheduleRuleForm, setMyScheduleRuleForm] = useState({ - data: null, - isValid: false, + const stepsData = useRef({ + [RuleStep.defineRule]: { isValid: false, data: undefined }, + [RuleStep.aboutRule]: { isValid: false, data: undefined }, + [RuleStep.scheduleRule]: { isValid: false, data: undefined }, + [RuleStep.ruleActions]: { isValid: false, data: undefined }, }); - const [myActionsRuleForm, setMyActionsRuleForm] = useState({ - data: null, - isValid: false, + const defineStep = stepsData.current[RuleStep.defineRule]; + const aboutStep = stepsData.current[RuleStep.aboutRule]; + const scheduleStep = stepsData.current[RuleStep.scheduleRule]; + const actionsStep = stepsData.current[RuleStep.ruleActions]; + const [activeStep, setActiveStep] = useState(RuleStep.defineRule); + const invalidSteps = ruleStepsOrder.filter((step) => { + const stepData = stepsData.current[step]; + return stepData.data != null && !stepIsValid(stepData); }); - const [selectedTab, setSelectedTab] = useState(); - const stepsForm = useRef | null>>({ - [RuleStep.defineRule]: null, - [RuleStep.aboutRule]: null, - [RuleStep.scheduleRule]: null, - [RuleStep.ruleActions]: null, - }); - const [{ isLoading, isSaved }, setRule] = usePersistRule(); - const [tabHasError, setTabHasError] = useState([]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule]); - const setStepsForm = useCallback( - (step: RuleStep, form: FormHook) => { - stepsForm.current[step] = form; - if (initForm && step === (selectedTab?.id as RuleStep) && form.isSubmitted === false) { - setInitForm(false); - form.submit(); + const [{ isLoading, isSaved }, setRule] = useUpdateRule(); + const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule?.type]); + const setFormHook = useCallback( + (step: K, hook: RuleStepsFormHooks[K]) => { + formHooks.current[step] = hook; + if (step === activeStep) { + hook(); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [initForm, selectedTab] + [activeStep] ); + const setStepData = useCallback( + (step: K, data: RuleStepsData[K], isValid: boolean) => { + stepsData.current[step] = { ...stepsData.current[step], data, isValid }; + }, + [] + ); + const tabs = useMemo( () => [ { @@ -135,14 +124,14 @@ const EditRulePageComponent: FC = () => { content: ( <> - - {myDefineRuleForm.data != null && ( + + {defineStep.data != null && ( )} @@ -157,15 +146,15 @@ const EditRulePageComponent: FC = () => { content: ( <> - - {myAboutRuleForm.data != null && myDefineRuleForm.data != null && ( + + {aboutStep.data != null && defineStep.data != null && ( )} @@ -180,14 +169,14 @@ const EditRulePageComponent: FC = () => { content: ( <> - - {myScheduleRuleForm.data != null && ( + + {scheduleStep.data != null && ( )} @@ -201,14 +190,14 @@ const EditRulePageComponent: FC = () => { content: ( <> - - {myActionsRuleForm.data != null && ( + + {actionsStep.data != null && ( )} @@ -218,76 +207,56 @@ const EditRulePageComponent: FC = () => { ), }, ], - // eslint-disable-next-line react-hooks/exhaustive-deps [ - rule, + rule?.immutable, loading, - initLoading, + defineStep.data, isLoading, - myAboutRuleForm, - myDefineRuleForm, - myScheduleRuleForm, - myActionsRuleForm, - setStepsForm, - stepsForm, + setFormHook, + aboutStep.data, + scheduleStep.data, + actionsStep.data, actionMessageParams, ] ); const onSubmit = useCallback(async () => { - const activeFormId = selectedTab?.id as RuleStep; - const activeForm = await stepsForm.current[activeFormId]?.submit(); - - const invalidForms = [ - RuleStep.aboutRule, - RuleStep.defineRule, - RuleStep.scheduleRule, - RuleStep.ruleActions, - ].reduce((acc, step) => { - if ( - (step === activeFormId && activeForm != null && !activeForm?.isValid) || - (step === RuleStep.aboutRule && !myAboutRuleForm.isValid) || - (step === RuleStep.defineRule && !myDefineRuleForm.isValid) || - (step === RuleStep.scheduleRule && !myScheduleRuleForm.isValid) || - (step === RuleStep.ruleActions && !myActionsRuleForm.isValid) - ) { - return [...acc, step]; - } - return acc; - }, []); + const activeStepData = await formHooks.current[activeStep](); + if (activeStepData?.data != null) { + setStepData(activeStep, activeStepData.data, activeStepData.isValid); + } + const define = isDefineStep(activeStepData) ? activeStepData : defineStep; + const about = isAboutStep(activeStepData) ? activeStepData : aboutStep; + const schedule = isScheduleStep(activeStepData) ? activeStepData : scheduleStep; + const actions = isActionsStep(activeStepData) ? activeStepData : actionsStep; - if (invalidForms.length === 0 && activeForm != null) { - setTabHasError([]); + if ( + stepIsValid(define) && + stepIsValid(about) && + stepIsValid(schedule) && + stepIsValid(actions) + ) { setRule({ - ...formatRule( - (activeFormId === RuleStep.defineRule - ? activeForm.data - : myDefineRuleForm.data) as DefineStepRule, - (activeFormId === RuleStep.aboutRule - ? activeForm.data - : myAboutRuleForm.data) as AboutStepRule, - (activeFormId === RuleStep.scheduleRule - ? activeForm.data - : myScheduleRuleForm.data) as ScheduleStepRule, - (activeFormId === RuleStep.ruleActions - ? activeForm.data - : myActionsRuleForm.data) as ActionsStepRule, + ...formatRule( + define.data, + about.data, + schedule.data, + actions.data, rule ), ...(ruleId ? { id: ruleId } : {}), }); - } else { - setTabHasError(invalidForms); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - stepsForm, - myAboutRuleForm, - myDefineRuleForm, - myScheduleRuleForm, - myActionsRuleForm, - selectedTab, + aboutStep, + actionsStep, + activeStep, + defineStep, + rule, ruleId, + scheduleStep, + setRule, + setStepData, ]); useEffect(() => { @@ -295,48 +264,29 @@ const EditRulePageComponent: FC = () => { const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ rule, }); - setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); - setMyDefineRuleForm({ data: defineRuleData, isValid: true }); - setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); - setMyActionsRuleForm({ data: ruleActionsData, isValid: true }); + setStepData(RuleStep.defineRule, defineRuleData, true); + setStepData(RuleStep.aboutRule, aboutRuleData, true); + setStepData(RuleStep.scheduleRule, scheduleRuleData, true); + setStepData(RuleStep.ruleActions, ruleActionsData, true); } - }, [rule]); + }, [rule, setStepData]); + + const goToStep = useCallback(async (step: RuleStep) => { + setActiveStep(step); + await formHooks.current[step](); + }, []); const onTabClick = useCallback( async (tab: EuiTabbedContentTab) => { - if (selectedTab != null) { - const ruleStep = selectedTab.id as RuleStep; - const respForm = await stepsForm.current[ruleStep]?.submit(); + const targetStep = tab.id as RuleStep; + const activeStepData = await formHooks.current[activeStep](); - if (respForm != null) { - if (ruleStep === RuleStep.aboutRule) { - setMyAboutRuleForm({ - data: respForm.data as AboutStepRule, - isValid: respForm.isValid, - }); - } else if (ruleStep === RuleStep.defineRule) { - setMyDefineRuleForm({ - data: respForm.data as DefineStepRule, - isValid: respForm.isValid, - }); - } else if (ruleStep === RuleStep.scheduleRule) { - setMyScheduleRuleForm({ - data: respForm.data as ScheduleStepRule, - isValid: respForm.isValid, - }); - } else if (ruleStep === RuleStep.ruleActions) { - setMyActionsRuleForm({ - data: respForm.data as ActionsStepRule, - isValid: respForm.isValid, - }); - } - } + if (activeStepData?.data != null) { + setStepData(activeStep, activeStepData.data, activeStepData.isValid); + goToStep(targetStep); } - setInitForm(true); - setSelectedTab(tab); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [selectedTab, stepsForm.current] + [activeStep, goToStep, setStepData] ); const goToDetailsRule = useCallback( @@ -348,23 +298,13 @@ const EditRulePageComponent: FC = () => { ); useEffect(() => { - if (rule != null) { - const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ - rule, - }); - setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); - setMyDefineRuleForm({ data: defineRuleData, isValid: true }); - setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); - setMyActionsRuleForm({ data: ruleActionsData, isValid: true }); + if (rule?.immutable) { + setActiveStep(RuleStep.ruleActions); + } else { + setActiveStep(RuleStep.defineRule); } }, [rule]); - useEffect(() => { - const tabIndex = rule?.immutable ? 3 : 0; - setSelectedTab(tabs[tabIndex]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rule]); - if (isSaved) { displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); history.replace(getRuleDetailsUrl(ruleId ?? '')); @@ -398,14 +338,14 @@ const EditRulePageComponent: FC = () => { isLoading={isLoading} title={i18n.PAGE_TITLE} /> - {tabHasError.length > 0 && ( + {invalidSteps.length > 0 && ( { if (t === RuleStep.aboutRule) { return ruleI18n.ABOUT; @@ -426,7 +366,7 @@ const EditRulePageComponent: FC = () => { t.id === selectedTab?.id)} + selectedTab={tabs.find((t) => t.id === activeStep)} onTabClick={onTabClick} tabs={tabs} /> @@ -451,7 +391,7 @@ const EditRulePageComponent: FC = () => { onClick={onSubmit} iconType="save" isLoading={isLoading} - isDisabled={initLoading} + isDisabled={loading} > {i18n.SAVE_CHANGES} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 10a20807d6f87..f11b0ac4ec3f8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -43,7 +43,6 @@ describe('rule helpers', () => { rule: mockRuleWithEverything('test-id'), }); const defineRuleStepData = { - isNew: false, ruleType: 'saved_query', anomalyThreshold: 50, index: ['auditbeat-*'], @@ -93,7 +92,6 @@ describe('rule helpers', () => { falsePositives: ['test'], isAssociatedToEndpointList: false, isBuildingBlock: false, - isNew: false, license: 'Elastic License', name: 'Query with rule-id', note: '# this is some markdown documentation', @@ -121,11 +119,10 @@ describe('rule helpers', () => { ], timestampOverride: 'event.ingested', }; - const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false }; + const scheduleRuleStepData = { from: '0s', interval: '5m' }; const ruleActionsStepData = { enabled: true, throttle: 'no_actions', - isNew: false, actions: [], }; const aboutRuleDataDetailsData = { @@ -202,7 +199,6 @@ describe('rule helpers', () => { test('returns with saved_id if value exists on rule', () => { const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); const expected = { - isNew: false, ruleType: 'saved_query', anomalyThreshold: 50, machineLearningJobId: '', @@ -235,7 +231,6 @@ describe('rule helpers', () => { delete mockedRule.saved_id; const result: DefineStepRule = getDefineStepsData(mockedRule); const expected = { - isNew: false, ruleType: 'saved_query', anomalyThreshold: 50, machineLearningJobId: '', @@ -311,7 +306,6 @@ describe('rule helpers', () => { }; const result: ScheduleStepRule = getScheduleStepsData(mockedRule); const expected = { - isNew: false, interval: mockedRule.interval, from: '0s', }; @@ -344,7 +338,6 @@ describe('rule helpers', () => { }, ], enabled: mockedRule.enabled, - isNew: false, throttle: 'no_actions', }; 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..aab73c5d5a1e5 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,11 @@ import { ScheduleStepRule, ActionsStepRule, } from './types'; -import { SeverityMapping } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { + SeverityMapping, + Type, + Severity, +} from '../../../../../common/detection_engine/schemas/common/schemas'; import { severityOptions } from '../../../components/rules/step_about_rule/data'; export interface GetStepsData { @@ -64,7 +68,6 @@ export const getActionsStepsData = ( return { actions: actions?.map(transformRuleToAlertAction), - isNew: false, throttle, kibanaSiemAppUrl: meta?.kibana_siem_app_url, enabled, @@ -72,7 +75,6 @@ export const getActionsStepsData = ( }; export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ - isNew: false, ruleType: rule.type, anomalyThreshold: rule.anomaly_threshold ?? 50, machineLearningJobId: rule.machine_learning_job_id ?? '', @@ -97,7 +99,6 @@ export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { const fromHumanizedValue = getHumanizedDuration(from, interval); return { - isNew: false, interval, from: fromHumanizedValue, }; @@ -139,7 +140,6 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu } = rule; return { - isNew: false, author, isAssociatedToEndpointList: exceptionsList?.some(({ id }) => id === ENDPOINT_LIST_ID) ?? false, isBuildingBlock: buildingBlockType !== undefined, @@ -151,7 +151,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu note: note!, references, severity: { - value: severity, + value: severity as Severity, mapping: fillEmptySeverityMappings(severityMapping), isMappingChecked: severityMapping.length > 0, }, @@ -307,7 +307,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 +321,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 +349,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/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 95ef85ec1317a..886a24dd7cbe8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import '../../../../common/mock/match_media'; import { RulesPage } from './index'; -import { useUserInfo } from '../../../components/user_info'; +import { useUserData } from '../../../components/user_info'; import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; jest.mock('react-router-dom', () => { @@ -30,7 +30,7 @@ jest.mock('../../../containers/detection_engine/rules'); describe('RulesPage', () => { beforeAll(() => { - (useUserInfo as jest.Mock).mockReturnValue({}); + (useUserData as jest.Mock).mockReturnValue([{}]); (usePrePackagedRules as jest.Mock).mockReturnValue({}); }); it('renders correctly', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 92ec0bb5a72cd..53c82569f94ae 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -18,7 +18,7 @@ import { DetectionEngineHeaderPage } from '../../../components/detection_engine_ import { WrapperPage } from '../../../../common/components/wrapper_page'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; -import { useUserInfo } from '../../../components/user_info'; +import { useUserData } from '../../../components/user_info'; import { AllRules } from './all'; import { ImportDataModal } from '../../../../common/components/import_data_modal'; import { ReadOnlyCallOut } from '../../../components/rules/read_only_callout'; @@ -42,14 +42,16 @@ const RulesPageComponent: React.FC = () => { const [showImportModal, setShowImportModal] = useState(false); const [showValueListsModal, setShowValueListsModal] = useState(false); const refreshRulesData = useRef(null); - const { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexWrite, - } = useUserInfo(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexWrite, + }, + ] = useUserData(); const { loading: listsConfigLoading, canWriteIndex: canWriteListsIndex, 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..e3d0ea123872a 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,10 +4,9 @@ * 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'; import { FieldValueQueryBar } from '../../../components/rules/query_bar'; import { FieldValueTimeline } from '../../../components/rules/pick_timeline'; import { FieldValueThreshold } from '../../../components/rules/threshold_input'; @@ -18,13 +17,16 @@ import { RiskScoreMapping, RuleNameOverride, SeverityMapping, + SortOrder, TimestampOverride, + Type, + Severity, } 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 { @@ -35,34 +37,51 @@ export interface EuiBasicTableOnChange { sort?: EuiBasicTableSortTypes; } +export type RuleStatusType = 'passive' | 'active' | 'valid'; + export enum RuleStep { defineRule = 'define-rule', aboutRule = 'about-rule', scheduleRule = 'schedule-rule', ruleActions = 'rule-actions', } -export type RuleStatusType = 'passive' | 'active' | 'valid'; +export type RuleStepsOrder = [ + RuleStep.defineRule, + RuleStep.aboutRule, + RuleStep.scheduleRule, + RuleStep.ruleActions +]; -export interface RuleStepData { - data: unknown; - isValid: boolean; +export interface RuleStepsData { + [RuleStep.defineRule]: DefineStepRule; + [RuleStep.aboutRule]: AboutStepRule; + [RuleStep.scheduleRule]: ScheduleStepRule; + [RuleStep.ruleActions]: ActionsStepRule; } +export type RuleStepsFormData = { + [K in keyof RuleStepsData]: { + data: RuleStepsData[K] | undefined; + isValid: boolean; + }; +}; + +export type RuleStepsFormHooks = { + [K in keyof RuleStepsData]: () => Promise; +}; + export interface RuleStepProps { addPadding?: boolean; descriptionColumns?: 'multi' | 'single' | 'singleSplit'; - setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; isReadOnlyView: boolean; isUpdateView?: boolean; isLoading: boolean; + onSubmit?: () => void; resizeParentContainer?: (height: number) => void; - setForm?: (step: RuleStep, form: FormHook) => void; + setForm?: (step: K, hook: RuleStepsFormHooks[K]) => void; } -interface StepRuleData { - isNew: boolean; -} -export interface AboutStepRule extends StepRuleData { +export interface AboutStepRule { author: string[]; name: string; description: string; @@ -86,7 +105,7 @@ export interface AboutStepRuleDetails { } export interface AboutStepSeverity { - value: string; + value: Severity; mapping: SeverityMapping; isMappingChecked: boolean; } @@ -97,23 +116,23 @@ export interface AboutStepRiskScore { isMappingChecked: boolean; } -export interface DefineStepRule extends StepRuleData { +export interface DefineStepRule { anomalyThreshold: number; index: string[]; machineLearningJobId: string; queryBar: FieldValueQueryBar; - ruleType: RuleType; + ruleType: Type; timeline: FieldValueTimeline; threshold: FieldValueThreshold; } -export interface ScheduleStepRule extends StepRuleData { +export interface ScheduleStepRule { interval: string; from: string; to?: string; } -export interface ActionsStepRule extends StepRuleData { +export interface ActionsStepRule { actions: AlertAction[]; enabled: boolean; kibanaSiemAppUrl?: string; @@ -134,7 +153,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/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index f862a06807e6f..890746838b0d0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -20,6 +20,14 @@ import { RouteSpyState } from '../../../../common/utils/route/types'; import { GetUrlForApp } from '../../../../common/components/navigation/types'; import { SecurityPageName } from '../../../../app/types'; import { APP_ID } from '../../../../../common/constants'; +import { RuleStep, RuleStepsOrder } from './types'; + +export const ruleStepsOrder: RuleStepsOrder = [ + RuleStep.defineRule, + RuleStep.aboutRule, + RuleStep.scheduleRule, + RuleStep.ruleActions, +]; const getTabBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetUrlForApp) => { const tabPath = pathname.split('/')[1]; diff --git a/x-pack/plugins/security_solution/public/detections/routes.tsx b/x-pack/plugins/security_solution/public/detections/routes.tsx index 8f542d1f88670..b5f7bc6983752 100644 --- a/x-pack/plugins/security_solution/public/detections/routes.tsx +++ b/x-pack/plugins/security_solution/public/detections/routes.tsx @@ -12,12 +12,11 @@ import { NotFoundPage } from '../app/404'; export const AlertsRoutes: React.FC = () => ( - ( - - )} - /> - } /> + + + + + + ); diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 7b20873bf63cc..b32083fec1b5e 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -4719,6 +4719,14 @@ "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "status", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index f7d2c81f536be..65d9212f77dcc 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -1020,6 +1020,8 @@ export interface SignalField { rule?: Maybe; original_time?: Maybe; + + status?: Maybe; } export interface RuleField { @@ -5098,6 +5100,8 @@ export namespace GetTimelineQuery { export type Signal = { __typename?: 'SignalField'; + status: Maybe; + original_time: Maybe; rule: Maybe<_Rule>; 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/components/first_last_seen_host/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx index a2f53be721816..606b43c6508fb 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx @@ -4,18 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { render, act, wait as waitFor } from '@testing-library/react'; -import { mockFirstLastSeenHostQuery } from '../../containers/hosts/first_last_seen/mock'; +import { useFirstLastSeenHost } from '../../containers/hosts/first_last_seen'; import { TestProviders } from '../../../common/mock'; - import { FirstLastSeenHost, FirstLastSeenHostType } from '.'; +const MOCKED_RESPONSE = { + firstSeen: '2019-04-08T16:09:40.692Z', + lastSeen: '2019-04-08T18:35:45.064Z', +}; + +jest.mock('../../containers/hosts/first_last_seen'); +const useFirstLastSeenHostMock = useFirstLastSeenHost as jest.Mock; +useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]); + describe('FirstLastSeen Component', () => { const firstSeen = 'Apr 8, 2019 @ 16:09:40.692'; const lastSeen = 'Apr 8, 2019 @ 18:35:45.064'; @@ -31,11 +37,10 @@ describe('FirstLastSeen Component', () => { }); test('Loading', async () => { + useFirstLastSeenHostMock.mockReturnValue([true, MOCKED_RESPONSE]); const { container } = render( - - - + ); expect(container.innerHTML).toBe( @@ -44,11 +49,10 @@ describe('FirstLastSeen Component', () => { }); test('First Seen', async () => { + useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]); const { container } = render( - - - + ); @@ -62,11 +66,10 @@ describe('FirstLastSeen Component', () => { }); test('Last Seen', async () => { + useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]); const { container } = render( - - - + ); await act(() => @@ -79,13 +82,16 @@ describe('FirstLastSeen Component', () => { }); test('First Seen is empty but not Last Seen', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.firstSeen = null; + useFirstLastSeenHostMock.mockReturnValue([ + false, + { + ...MOCKED_RESPONSE, + firstSeen: null, + }, + ]); const { container } = render( - - - + ); @@ -99,13 +105,16 @@ describe('FirstLastSeen Component', () => { }); test('Last Seen is empty but not First Seen', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.lastSeen = null; + useFirstLastSeenHostMock.mockReturnValue([ + false, + { + ...MOCKED_RESPONSE, + lastSeen: null, + }, + ]); const { container } = render( - - - + ); @@ -119,13 +128,16 @@ describe('FirstLastSeen Component', () => { }); test('First Seen With a bad date time string', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.firstSeen = 'something-invalid'; + useFirstLastSeenHostMock.mockReturnValue([ + false, + { + ...MOCKED_RESPONSE, + firstSeen: 'something-invalid', + }, + ]); const { container } = render( - - - + ); await act(() => @@ -136,13 +148,16 @@ describe('FirstLastSeen Component', () => { }); test('Last Seen With a bad date time string', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.lastSeen = 'something-invalid'; + useFirstLastSeenHostMock.mockReturnValue([ + false, + { + ...MOCKED_RESPONSE, + lastSeen: 'something-invalid', + }, + ]); const { container } = render( - - - + ); await act(() => diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx index 579c3311cf732..a1b72fb39069c 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx @@ -5,10 +5,9 @@ */ import { EuiIcon, EuiLoadingSpinner, EuiText, EuiToolTip } from '@elastic/eui'; -import React from 'react'; -import { ApolloConsumer } from 'react-apollo'; +import React, { useMemo } from 'react'; -import { useFirstLastSeenHostQuery } from '../../containers/hosts/first_last_seen'; +import { useFirstLastSeenHost } from '../../containers/hosts/first_last_seen'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; @@ -17,49 +16,48 @@ export enum FirstLastSeenHostType { LAST_SEEN = 'last-seen', } -export const FirstLastSeenHost = React.memo<{ hostname: string; type: FirstLastSeenHostType }>( - ({ hostname, type }) => { +interface FirstLastSeenHostProps { + hostName: string; + type: FirstLastSeenHostType; +} + +export const FirstLastSeenHost = React.memo(({ hostName, type }) => { + const [loading, { firstSeen, lastSeen, errorMessage }] = useFirstLastSeenHost({ + hostName, + }); + const valueSeen = useMemo( + () => (type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen), + [firstSeen, lastSeen, type] + ); + + if (errorMessage != null) { return ( - - {(client) => { - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - const { loading, firstSeen, lastSeen, errorMessage } = useFirstLastSeenHostQuery( - hostname, - 'default', - client - ); - if (errorMessage != null) { - return ( - - - - ); - } - const valueSeen = type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen; - return ( - <> - {loading && } - {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date' - ? valueSeen - : !loading && - valueSeen != null && ( - - - - )} - {!loading && valueSeen == null && getEmptyTagValue()} - - ); - }} - + + + ); } -); + + return ( + <> + {loading && } + {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date' + ? valueSeen + : !loading && + valueSeen != null && ( + + + + )} + {!loading && valueSeen == null && getEmptyTagValue()} + + ); +}); FirstLastSeenHost.displayName = 'FirstLastSeenHost'; 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.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts deleted file mode 100644 index 65e379b5ba2d8..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts +++ /dev/null @@ -1,88 +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 ApolloClient from 'apollo-client'; -import { get } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; -import { useUiSetting$ } from '../../../../common/lib/kibana'; -import { GetHostFirstLastSeenQuery } from '../../../../graphql/types'; -import { inputsModel } from '../../../../common/store'; -import { QueryTemplateProps } from '../../../../common/containers/query_template'; -import { useWithSource } from '../../../../common/containers/source'; -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -export interface FirstLastSeenHostArgs { - id: string; - errorMessage: string; - firstSeen: Date; - lastSeen: Date; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: FirstLastSeenHostArgs) => React.ReactNode; - hostName: string; -} - -export function useFirstLastSeenHostQuery( - hostName: string, - sourceId: string, - apolloClient: ApolloClient -) { - const [loading, updateLoading] = useState(false); - const [firstSeen, updateFirstSeen] = useState(null); - const [lastSeen, updateLastSeen] = useState(null); - const [errorMessage, updateErrorMessage] = useState(null); - const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const { docValueFields } = useWithSource(sourceId); - - async function fetchFirstLastSeenHost(signal: AbortSignal) { - updateLoading(true); - return apolloClient - .query({ - query: HostFirstLastSeenGqlQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - hostName, - defaultIndex, - docValueFields, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - (result) => { - updateLoading(false); - updateFirstSeen(get('data.source.HostFirstLastSeen.firstSeen', result)); - updateLastSeen(get('data.source.HostFirstLastSeen.lastSeen', result)); - updateErrorMessage(null); - }, - (error) => { - updateLoading(false); - updateFirstSeen(null); - updateLastSeen(null); - updateErrorMessage(error.message); - } - ); - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchFirstLastSeenHost(signal); - return () => abortCtrl.abort(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return { firstSeen, lastSeen, loading, errorMessage }; -} 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 new file mode 100644 index 0000000000000..169fe58e9a2cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx @@ -0,0 +1,139 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { + HostsQueries, + HostFirstLastSeenRequestOptions, + HostFirstLastSeenStrategyResponse, +} from '../../../../../common/search_strategy/security_solution'; +import { useWithSource } from '../../../../common/containers/source'; + +import * as i18n from './translations'; +import { AbortError } from '../../../../../../../../src/plugins/data/common'; + +const ID = 'firstLastSeenHostQuery'; + +export interface FirstLastSeenHostArgs { + id: string; + errorMessage: string | null; + firstSeen?: string | null; + lastSeen?: string | null; +} +interface UseHostFirstLastSeen { + hostName: string; +} + +export const useFirstLastSeenHost = ({ + hostName, +}: UseHostFirstLastSeen): [boolean, FirstLastSeenHostArgs] => { + const { docValueFields } = useWithSource('default'); + const { data, notifications, uiSettings } = useKibana().services; + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [firstLastSeenHostRequest, setFirstLastSeenHostRequest] = useState< + HostFirstLastSeenRequestOptions + >({ + defaultIndex, + docValueFields: docValueFields ?? [], + factoryQueryType: HostsQueries.firstLastSeen, + hostName, + }); + + const [firstLastSeenHostResponse, setFirstLastSeenHostResponse] = useState( + { + firstSeen: null, + lastSeen: null, + errorMessage: null, + id: ID, + } + ); + + const firstLastSeenHostSearch = useCallback( + (request: HostFirstLastSeenRequestOptions) => { + 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); + setFirstLastSeenHostResponse((prevResponse) => ({ + ...prevResponse, + errorMessage: null, + firstSeen: response.firstSeen, + lastSeen: response.lastSeen, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_FIRST_LAST_SEEN_HOST); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + setFirstLastSeenHostResponse((prevResponse) => ({ + ...prevResponse, + errorMessage: msg, + })); + notifications.toasts.addDanger({ + title: i18n.FAIL_FIRST_LAST_SEEN_HOST, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); + + useEffect(() => { + setFirstLastSeenHostRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + docValueFields: docValueFields ?? [], + hostName, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [defaultIndex, docValueFields, hostName]); + + useEffect(() => { + firstLastSeenHostSearch(firstLastSeenHostRequest); + }, [firstLastSeenHostRequest, firstLastSeenHostSearch]); + + return [loading, firstLastSeenHostResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts deleted file mode 100644 index 7f1b3d97eb525..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts +++ /dev/null @@ -1,53 +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 { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; -import { GetHostFirstLastSeenQuery } from '../../../../graphql/types'; - -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -interface MockedProvidedQuery { - request: { - query: GetHostFirstLastSeenQuery.Query; - variables: GetHostFirstLastSeenQuery.Variables; - }; - result: { - data?: { - source: { - id: string; - HostFirstLastSeen: { - firstSeen: string | null; - lastSeen: string | null; - }; - }; - }; - errors?: [{ message: string }]; - }; -} -export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ - { - request: { - query: HostFirstLastSeenGqlQuery, - variables: { - sourceId: 'default', - hostName: 'kibana-siem', - defaultIndex: DEFAULT_INDEX_PATTERN, - docValueFields: [], - }, - }, - result: { - data: { - source: { - id: 'default', - HostFirstLastSeen: { - firstSeen: '2019-04-08T16:09:40.692Z', - lastSeen: '2019-04-08T18:35:45.064Z', - }, - }, - }, - }, - }, -]; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/translations.ts new file mode 100644 index 0000000000000..1e0a4ad237897 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/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_FIRST_LAST_SEEN_HOST = i18n.translate( + 'xpack.securitySolution.firstLastSeenHost.errorSearchDescription', + { + defaultMessage: `An error has occurred on first last seen host search`, + } +); + +export const FAIL_FIRST_LAST_SEEN_HOST = i18n.translate( + 'xpack.securitySolution.firstLastSeenHost.failSearchDescription', + { + defaultMessage: `Failed to run search on first last seen host`, + } +); 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 0410f630173e1..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 @@ -22,11 +22,13 @@ import { 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'; @@ -35,7 +37,7 @@ export interface HostsArgs { endDate: string; hosts: HostsEdges[]; id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; loadPage: LoadPage; pageInfo: PageInfoPaginated; @@ -48,6 +50,7 @@ interface UseAllHost { docValueFields?: DocValueFields[]; filterQuery?: ESTermQuery | string; endDate: string; + skip?: boolean; startDate: string; type: hostsModel.HostsType; } @@ -56,6 +59,7 @@ export const useAllHost = ({ docValueFields, filterQuery, endDate, + skip = false, startDate, type, }: UseAllHost): [boolean, HostsArgs] => { @@ -128,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) => { @@ -138,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, @@ -190,7 +194,7 @@ export const useAllHost = ({ field: sortField, }, }; - if (!deepEqual(prevRequest, myRequest)) { + if (!skip && !deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; @@ -203,6 +207,7 @@ export const useAllHost = ({ endDate, filterQuery, limit, + skip, startDate, sortField, ]); 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 new file mode 100644 index 0000000000000..b28f479634d42 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx @@ -0,0 +1,161 @@ +/* + * 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. + */ + +// REPLACE WHEN HOST ENDPOINT DATA IS AVAILABLE + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { inputsModel } from '../../../../common/store'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + HostItem, + HostsQueries, + HostOverviewRequestOptions, + HostOverviewStrategyResponse, +} from '../../../../../common/search_strategy/security_solution/hosts'; + +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: InspectResponse; + hostOverview: HostItem; + refetch: inputsModel.Refetch; + startDate: string; + endDate: string; +} + +interface UseHostOverview { + id?: string; + hostName: string; + endDate: string; + skip?: boolean; + startDate: string; +} + +export const useHostOverview = ({ + endDate, + hostName, + skip = false, + startDate, + id = ID, +}: UseHostOverview): [boolean, HostOverviewArgs] => { + 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 [hostOverviewRequest, setHostOverviewRequest] = useState({ + defaultIndex, + hostName, + factoryQueryType: HostsQueries.hostOverview, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + + const [hostOverviewResponse, setHostOverviewResponse] = useState({ + endDate, + hostOverview: {}, + id: ID, + inspect: { + dsl: [], + response: [], + }, + refetch: refetch.current, + startDate, + }); + + const hostOverviewSearch = useCallback( + (request: HostOverviewRequestOptions) => { + 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); + setHostOverviewResponse((prevResponse) => ({ + ...prevResponse, + hostOverview: response.hostOverview, + inspect: getInspectResponse(response, prevResponse.inspect), + refetch: refetch.current, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_HOST_OVERVIEW); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_HOST_OVERVIEW, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); + + useEffect(() => { + setHostOverviewRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + hostName, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [defaultIndex, endDate, hostName, startDate, skip]); + + useEffect(() => { + hostOverviewSearch(hostOverviewRequest); + }, [hostOverviewRequest, hostOverviewSearch]); + + return [loading, hostOverviewResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/translations.ts new file mode 100644 index 0000000000000..e3fa319e70cc1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/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_HOST_OVERVIEW = i18n.translate( + 'xpack.securitySolution.overviewHost.errorSearchDescription', + { + defaultMessage: `An error has occurred on host overview search`, + } +); + +export const FAIL_HOST_OVERVIEW = i18n.translate( + 'xpack.securitySolution.overviewHost.failSearchDescription', + { + defaultMessage: `Failed to run search on host overview`, + } +); 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 88886a874a949..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,13 +8,13 @@ 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 { MatrixHistogramOption, MatrixHistogramMappingTypes, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { KpiHostsChartColors } from '../../components/kpi_hosts/types'; @@ -49,7 +49,7 @@ export const authMatrixDataMappingFields: MatrixHistogramMappingTypes = { }, }; -const histogramConfigs: MatrixHisrogramConfigs = { +const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: authStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? authStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA, @@ -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/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index cea987db485f4..f28c3dfa1ad77 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -14,14 +14,13 @@ import { hostsModel } from '../../store'; import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model'; import { MatrixHistogramOption, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; import { HistogramType } from '../../../graphql/types'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; @@ -42,7 +41,7 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = 'event.action'; -export const histogramConfigs: MatrixHisrogramConfigs = { +export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: eventsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA, @@ -52,14 +51,14 @@ export const histogramConfigs: MatrixHisrogramConfigs = { title: i18n.NAVIGATION_EVENTS_TITLE, }; -export const EventsQueryTabBody = ({ +const EventsQueryTabBodyComponent: React.FC = ({ deleteQuery, endDate, filterQuery, pageFilters, setQuery, startDate, -}: HostsComponentsQueryProps) => { +}) => { const { initializeTimeline } = useManageTimeline(); const dispatch = useDispatch(); const { globalFullScreen } = useFullScreen(); @@ -67,9 +66,6 @@ export const EventsQueryTabBody = ({ initializeTimeline({ id: TimelineId.hostsPageEvents, defaultModel: eventsDefaultModel, - timelineRowActions: () => [ - getInvestigateInResolverAction({ dispatch, timelineId: TimelineId.hostsPageEvents }), - ], }); }, [dispatch, initializeTimeline]); @@ -106,4 +102,8 @@ export const EventsQueryTabBody = ({ ); }; +EventsQueryTabBodyComponent.displayName = 'EventsQueryTabBodyComponent'; + +export const EventsQueryTabBody = React.memo(EventsQueryTabBodyComponent); + EventsQueryTabBody.displayName = 'EventsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx index 5232dcfd88189..f8dcf9635c053 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx @@ -27,7 +27,8 @@ export const HostsQueryTabBody = ({ const [ loading, { hosts, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, - ] = useAllHost({ docValueFields, endDate, filterQuery, startDate, type }); + ] = useAllHost({ docValueFields, endDate, filterQuery, skip, startDate, type }); + return ( ( - - {({ - 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/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx index bbf1c4eeacca7..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 @@ -16,8 +16,8 @@ import { FlowTargetSourceDest, NetworkTopCountriesEdges, NetworkTopTablesFields, - NetworkTopTablesSortField, -} from '../../../../common/search_strategy/security_solution/network'; + SortField, +} from '../../../../common/search_strategy'; import { State } from '../../../common/store'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; @@ -126,12 +126,12 @@ const NetworkTopCountriesTableComponent: React.FC (criteria: Criteria) => { if (criteria.sort != null) { const splitField = criteria.sort.field.split('.'); - const lastField = last(splitField); + const lastField = last(splitField) as NetworkTopTablesFields; 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, + 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( diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/_index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/_index.tsx deleted file mode 100644 index be792d0647fa6..0000000000000 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/_index.tsx +++ /dev/null @@ -1,168 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { - GetNetworkDnsQuery, - NetworkDnsEdges, - NetworkDnsSortField, - PageInfoPaginated, - MatrixOverOrdinalHistogramData, -} from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } 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 { networkDnsQuery } from './index.gql_query'; - -import { networkModel, networkSelectors } from '../../store'; - -const ID = 'networkDnsQuery'; -export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; -export interface NetworkDnsArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkDns: NetworkDnsEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - stackByField?: string; - totalCount: number; - histogram: MatrixOverOrdinalHistogramData[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkDnsArgs) => React.ReactNode; - type: networkModel.NetworkType; -} - -export interface NetworkDnsComponentReduxProps { - activePage: number; - sort: NetworkDnsSortField; - isInspected: boolean; - isPtrIncluded: boolean; - limit: number; -} - -type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps & WithKibanaProps; - -export class NetworkDnsComponentQuery extends QueryTemplatePaginated< - NetworkDnsProps, - GetNetworkDnsQuery.Query, - GetNetworkDnsQuery.Variables -> { - public render() { - const { - activePage, - children, - sort, - endDate, - filterQuery, - id = ID, - isInspected, - isPtrIncluded, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetNetworkDnsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - isPtrIncluded, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkDnsQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkDns = getOr([], `source.NetworkDns.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkDns: { - ...fetchMoreResult.source.NetworkDns, - edges: [...fetchMoreResult.source.NetworkDns.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkDns.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkDns, - pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), - histogram: getOr(null, 'source.NetworkDns.histogram', data), - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - isInspected, - id, - }; - }; - - return mapStateToProps; -}; - -export const NetworkDnsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkDnsComponentQuery); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 7963f866f7599..53d9a303ab849 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -25,6 +25,8 @@ import { } from '../../../../common/search_strategy/security_solution/network'; import { AbortError } from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; export * from './histogram'; @@ -32,7 +34,7 @@ const ID = 'networkDnsQuery'; export interface NetworkDnsArgs { id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; loadPage: (newActivePage: number) => void; networkDns: NetworkDnsEdges[]; @@ -60,8 +62,6 @@ export const useNetworkDns = ({ startDate, type, }: UseNetworkDns): [boolean, NetworkDnsArgs] => { - // const getQuery = inputsSelectors.globalQueryByIdSelector(); - // const { isInspected } = useSelector((state: State) => getQuery(state, id), shallowEqual); const getNetworkDnsSelector = networkSelectors.dnsSelector(); const { activePage, sort, isPtrIncluded, limit } = useSelector( (state: State) => getNetworkDnsSelector(state), @@ -78,9 +78,8 @@ export const useNetworkDns = ({ factoryQueryType: NetworkQueries.dns, filterQuery: createFilter(filterQuery), isPtrIncluded, - // inspect: isInspected, pagination: generateTablePaginationOptions(activePage, limit), - networkDnsSortField: sort, + sort, timerange: { interval: '12h', from: startDate ? startDate : '', @@ -127,7 +126,7 @@ export const useNetworkDns = ({ const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', - signal: abortCtrl.current.signal, + abortSignal: abortCtrl.current.signal, }) .subscribe({ next: (response) => { @@ -137,7 +136,7 @@ export const useNetworkDns = ({ setNetworkDnsResponse((prevResponse) => ({ ...prevResponse, networkDns: response.edges, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), pageInfo: response.pageInfo, refetch: refetch.current, totalCount: response.totalCount, @@ -187,12 +186,12 @@ export const useNetworkDns = ({ isPtrIncluded, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), + sort, timerange: { interval: '12h', from: startDate, to: endDate, }, - networkDnsSortField: sort, }; if (!deepEqual(prevRequest, myRequest)) { return myRequest; 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 3ca098abe8f17..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 @@ -21,16 +21,19 @@ import { NetworkQueries, NetworkHttpRequestOptions, NetworkHttpStrategyResponse, -} from '../../../../common/search_strategy/security_solution/network'; + 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; loadPage: (newActivePage: number) => void; networkHttp: NetworkHttpEdges[]; @@ -58,8 +61,6 @@ export const useNetworkHttp = ({ startDate, type, }: UseNetworkHttp): [boolean, NetworkHttpArgs] => { - // const getQuery = inputsSelectors.globalQueryByIdSelector(); - // const { isInspected } = useSelector((state: State) => getQuery(state, id), shallowEqual); const getHttpSelector = networkSelectors.httpSelector(); const { activePage, limit, sort } = useSelector( (state: State) => getHttpSelector(state, type), @@ -75,10 +76,9 @@ export const useNetworkHttp = ({ defaultIndex, factoryQueryType: NetworkQueries.http, filterQuery: createFilter(filterQuery), - // inspect: isInspected, ip, pagination: generateTablePaginationOptions(activePage, limit), - networkHttpSort: sort, + sort: sort as SortField, timerange: { interval: '12h', from: startDate ? startDate : '', @@ -126,7 +126,7 @@ export const useNetworkHttp = ({ const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', - signal: abortCtrl.current.signal, + abortSignal: abortCtrl.current.signal, }) .subscribe({ next: (response) => { @@ -136,7 +136,7 @@ export const useNetworkHttp = ({ setNetworkHttpResponse((prevResponse) => ({ ...prevResponse, networkHttp: response.edges, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), pageInfo: response.pageInfo, refetch: refetch.current, totalCount: response.totalCount, @@ -174,24 +174,20 @@ export const useNetworkHttp = ({ ); useEffect(() => { - if (skip) { - return; - } - setHostRequest((prevRequest) => { const myRequest = { ...prevRequest, defaultIndex, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), + sort: sort as SortField, timerange: { interval: '12h', from: startDate, to: endDate, }, - networkHttpSort: sort, }; - if (!deepEqual(prevRequest, myRequest)) { + if (!skip && !deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; 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 14696e640945f..747f5e4f502dd 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 @@ -14,7 +14,6 @@ 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 { PageInfoPaginated } from '../../../../common/search_strategy/security_solution'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; import { networkModel, networkSelectors } from '../../store'; import { @@ -23,15 +22,18 @@ import { NetworkTopCountriesEdges, NetworkTopCountriesRequestOptions, NetworkTopCountriesStrategyResponse, -} from '../../../../common/search_strategy/security_solution/network'; + PageInfoPaginated, +} 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; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; loadPage: (newActivePage: number) => void; pageInfo: PageInfoPaginated; @@ -48,20 +50,16 @@ interface UseNetworkTopCountries { endDate: string; startDate: string; skip: boolean; - id?: string; } export const useNetworkTopCountries = ({ endDate, filterQuery, flowTarget, - id = ID, skip, startDate, type, }: UseNetworkTopCountries): [boolean, NetworkTopCountriesArgs] => { - // const getQuery = inputsSelectors.globalQueryByIdSelector(); - // const { isInspected } = useSelector((state: State) => getQuery(state, id), shallowEqual); const getTopCountriesSelector = networkSelectors.topCountriesSelector(); const { activePage, limit, sort } = useSelector( (state: State) => getTopCountriesSelector(state, type, flowTarget), @@ -78,9 +76,8 @@ export const useNetworkTopCountries = ({ factoryQueryType: NetworkQueries.topCountries, filterQuery: createFilter(filterQuery), flowTarget, - // inspect: isInspected, pagination: generateTablePaginationOptions(activePage, limit), - networkTopCountriesSort: sort, + sort, timerange: { interval: '12h', from: startDate ? startDate : '', @@ -102,7 +99,7 @@ export const useNetworkTopCountries = ({ NetworkTopCountriesArgs >({ networkTopCountries: [], - id: ID, + id: `${ID}-${flowTarget}`, inspect: { dsl: [], response: [], @@ -128,7 +125,7 @@ export const useNetworkTopCountries = ({ const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', - signal: abortCtrl.current.signal, + abortSignal: abortCtrl.current.signal, }) .subscribe({ next: (response) => { @@ -138,7 +135,7 @@ export const useNetworkTopCountries = ({ setNetworkTopCountriesResponse((prevResponse) => ({ ...prevResponse, networkTopCountries: response.edges, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), pageInfo: response.pageInfo, refetch: refetch.current, totalCount: response.totalCount, @@ -176,24 +173,20 @@ export const useNetworkTopCountries = ({ ); useEffect(() => { - if (skip) { - return; - } - setHostRequest((prevRequest) => { const myRequest = { ...prevRequest, defaultIndex, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), + sort, timerange: { interval: '12h', from: startDate, to: endDate, }, - networkTopCountriesSort: sort, }; - if (!deepEqual(prevRequest, myRequest)) { + if (!skip && !deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index 86e366b253dbe..cc0da816c57ec 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -14,7 +14,6 @@ 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 { PageInfoPaginated } from '../../../../common/search_strategy/security_solution'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; import { networkModel, networkSelectors } from '../../store'; import { @@ -23,15 +22,18 @@ import { NetworkTopNFlowEdges, NetworkTopNFlowRequestOptions, NetworkTopNFlowStrategyResponse, -} from '../../../../common/search_strategy/security_solution/network'; + PageInfoPaginated, +} 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 = 'networkTopNFlowQuery'; export interface NetworkTopNFlowArgs { id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; loadPage: (newActivePage: number) => void; pageInfo: PageInfoPaginated; @@ -48,20 +50,16 @@ interface UseNetworkTopNFlow { endDate: string; startDate: string; skip: boolean; - id?: string; } export const useNetworkTopNFlow = ({ endDate, filterQuery, flowTarget, - id = ID, skip, startDate, type, }: UseNetworkTopNFlow): [boolean, NetworkTopNFlowArgs] => { - // const getQuery = inputsSelectors.globalQueryByIdSelector(); - // const { isInspected } = useSelector((state: State) => getQuery(state, id), shallowEqual); const getTopNFlowSelector = networkSelectors.topNFlowSelector(); const { activePage, limit, sort } = useSelector( (state: State) => getTopNFlowSelector(state, type, flowTarget), @@ -78,9 +76,8 @@ export const useNetworkTopNFlow = ({ factoryQueryType: NetworkQueries.topNFlow, filterQuery: createFilter(filterQuery), flowTarget, - // inspect: isInspected, pagination: generateTablePaginationOptions(activePage, limit), - networkTopNFlowSort: sort, + sort, timerange: { interval: '12h', from: startDate ? startDate : '', @@ -100,7 +97,7 @@ export const useNetworkTopNFlow = ({ const [networkTopNFlowResponse, setNetworkTopNFlowResponse] = useState({ networkTopNFlow: [], - id: ID, + id: `${ID}-${flowTarget}`, inspect: { dsl: [], response: [], @@ -126,7 +123,7 @@ export const useNetworkTopNFlow = ({ const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', - signal: abortCtrl.current.signal, + abortSignal: abortCtrl.current.signal, }) .subscribe({ next: (response) => { @@ -136,7 +133,7 @@ export const useNetworkTopNFlow = ({ setNetworkTopNFlowResponse((prevResponse) => ({ ...prevResponse, networkTopNFlow: response.edges, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), pageInfo: response.pageInfo, refetch: refetch.current, totalCount: response.totalCount, @@ -174,10 +171,6 @@ export const useNetworkTopNFlow = ({ ); useEffect(() => { - if (skip) { - return; - } - setTopNFlowRequest((prevRequest) => { const myRequest = { ...prevRequest, @@ -189,9 +182,9 @@ export const useNetworkTopNFlow = ({ from: startDate, to: endDate, }, - networkTopNFlowSort: sort, + sort, }; - if (!deepEqual(prevRequest, myRequest)) { + if (!skip && !deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; 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 ee5d74a3c0eff..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'; @@ -59,8 +61,6 @@ export const useNetworkTls = ({ startDate, type, }: UseNetworkTls): [boolean, NetworkTlsArgs] => { - // const getQuery = inputsSelectors.globalQueryByIdSelector(); - // const { isInspected } = useSelector((state: State) => getQuery(state, id), shallowEqual); const getTlsSelector = networkSelectors.tlsSelector(); const { activePage, limit, sort } = useSelector( (state: State) => getTlsSelector(state, type, flowTarget), @@ -77,7 +77,6 @@ export const useNetworkTls = ({ factoryQueryType: NetworkQueries.tls, filterQuery: createFilter(filterQuery), flowTarget, - // inspect: isInspected, ip, pagination: generateTablePaginationOptions(activePage, limit), sort, @@ -90,12 +89,10 @@ export const useNetworkTls = ({ const wrappedLoadMore = useCallback( (newActivePage: number) => { - setHostRequest((prevRequest) => { - return { - ...prevRequest, - pagination: generateTablePaginationOptions(newActivePage, limit), - }; - }); + setHostRequest((prevRequest) => ({ + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + })); }, [limit] ); @@ -128,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) => { @@ -138,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, @@ -173,10 +170,6 @@ export const useNetworkTls = ({ ); useEffect(() => { - if (skip) { - return; - } - setHostRequest((prevRequest) => { const myRequest = { ...prevRequest, @@ -190,7 +183,7 @@ export const useNetworkTls = ({ }, sort, }; - if (!deepEqual(prevRequest, myRequest)) { + if (!skip && !deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; 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 d1ee48a9a5d9e..cab6e8e09b200 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 @@ -11,7 +11,7 @@ import { NetworkType } from '../../store/model'; import { FlowTarget, FlowTargetSourceDest, -} from '../../../../common/search_strategy/security_solution/network'; +} from '../../../../common/search_strategy/security_solution'; 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/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 10cc11258cbff..4d88151a47309 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -16,7 +16,7 @@ import { networkModel } from '../../store'; import { MatrixHistogramOption, - MatrixHisrogramConfigs, + MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import * as i18n from '../translations'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; @@ -33,7 +33,7 @@ const dnsStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = 'dns.question.registered_domain'; -export const histogramConfigs: Omit = { +export const histogramConfigs: Omit = { defaultStackByOption: dnsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? dnsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_DNS_DATA, @@ -75,7 +75,7 @@ const DnsQueryTabBodyComponent: React.FC = ({ [] ); - const dnsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const dnsHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, title: getTitle, diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx index 1e49f9ca747c3..279891cc181e3 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx @@ -12,7 +12,7 @@ import { TlsQueryTabBodyProps } from './types'; const TlsTableManage = manageQuery(TlsTable); -export const TlsQueryTabBody = ({ +const TlsQueryTabBodyComponent: React.FC = ({ endDate, filterQuery, flowTarget, @@ -21,7 +21,7 @@ export const TlsQueryTabBody = ({ skip, startDate, type, -}: TlsQueryTabBodyProps) => { +}) => { const [ loading, { id, inspect, isInspected, tls, totalCount, pageInfo, loadPage, refetch }, @@ -52,3 +52,7 @@ export const TlsQueryTabBody = ({ /> ); }; + +TlsQueryTabBodyComponent.displayName = 'TlsQueryTabBodyComponent'; + +export const TlsQueryTabBody = React.memo(TlsQueryTabBodyComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index 6e59d81a1eae9..111935782949b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -26,7 +26,7 @@ import { alertsStackByOptions, histogramConfigs, } from '../../../common/components/alerts_viewer/histogram_configs'; -import { MatrixHisrogramConfigs } from '../../../common/components/matrix_histogram/types'; +import { MatrixHistogramConfigs } from '../../../common/components/matrix_histogram/types'; import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; @@ -93,7 +93,7 @@ const AlertsByCategoryComponent: React.FC = ({ [goToHostAlerts, formatUrl] ); - const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const alertsByCategoryHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, defaultStackByOption: diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index f18fccee50e22..2e9c25f01b3c1 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -14,7 +14,7 @@ import { SHOWING, UNIT } from '../../../common/components/events_viewer/translat import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { - MatrixHisrogramConfigs, + MatrixHistogramConfigs, MatrixHistogramOption, } from '../../../common/components/matrix_histogram/types'; import { eventsStackByOptions } from '../../../hosts/pages/navigation'; @@ -127,7 +127,7 @@ const EventsByDatasetComponent: React.FC = ({ [combinedQueries, kibana, indexPattern, query, filters] ); - const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( + const eventsByDatasetHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, stackByOptions: diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index c7aba6fcc8a5b..08f3f01bc99f6 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -91,7 +91,7 @@ export const HostOverview = React.memo( description: data.host != null && data.host.name && data.host.name.length ? ( ) : ( @@ -103,7 +103,7 @@ export const HostOverview = React.memo( description: data.host != null && data.host.name && data.host.name.length ? ( ) : ( diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/__snapshots__/index.test.tsx.snap index 8d4de5b90fae9..23732e88ba1f9 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/__snapshots__/index.test.tsx.snap @@ -40,6 +40,8 @@ exports[`Overview Host Stat Data rendering it renders the default OverviewHostSt buttonContentClassName="accordion-button" id="host-stat-accordion-groupauditbeat" initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} paddingSize="none" > @@ -273,6 +275,8 @@ exports[`Overview Host Stat Data rendering it renders the default OverviewHostSt buttonContentClassName="accordion-button" id="host-stat-accordion-groupendgame" initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} paddingSize="none" > @@ -538,6 +542,8 @@ exports[`Overview Host Stat Data rendering it renders the default OverviewHostSt buttonContentClassName="accordion-button" id="host-stat-accordion-groupfilebeat" initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} paddingSize="none" > @@ -611,6 +617,8 @@ exports[`Overview Host Stat Data rendering it renders the default OverviewHostSt buttonContentClassName="accordion-button" id="host-stat-accordion-groupwinlogbeat" initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} paddingSize="none" > diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/__snapshots__/index.test.tsx.snap index f6060fc060958..167866ef3c606 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/__snapshots__/index.test.tsx.snap @@ -40,6 +40,8 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet buttonContentClassName="accordion-button" id="network-stat-accordion-groupauditbeat" initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} paddingSize="none" > @@ -113,6 +115,8 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet buttonContentClassName="accordion-button" id="network-stat-accordion-groupfilebeat" initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} paddingSize="none" > @@ -314,6 +318,8 @@ exports[`Overview Network Stat Data rendering it renders the default OverviewNet buttonContentClassName="accordion-button" id="network-stat-accordion-grouppacketbeat" initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} paddingSize="none" > 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/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index 097166a9c866a..08e9fb854e5a2 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -21,6 +21,7 @@ export { UseMultiFields, useForm, useFormContext, + useFormData, ValidationFunc, VALIDATION_TYPES, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx index 07c4893e4550b..3c9101878be8d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx @@ -46,13 +46,7 @@ PanesFlexGroup.displayName = 'PanesFlexGroup'; type Props = Pick< FieldBrowserProps, - | 'browserFields' - | 'isEventViewer' - | 'height' - | 'onFieldSelected' - | 'onUpdateColumns' - | 'timelineId' - | 'width' + 'browserFields' | 'height' | 'onFieldSelected' | 'onUpdateColumns' | 'timelineId' | 'width' > & { /** * The current timeline column headers @@ -106,7 +100,6 @@ const FieldsBrowserComponent: React.FC = ({ browserFields, columnHeaders, filteredBrowserFields, - isEventViewer, isSearching, onCategorySelected, onFieldSelected, @@ -193,7 +186,6 @@ const FieldsBrowserComponent: React.FC = ({
    { ); - expect(wrapper.find('strong').first().text()).toEqual(highlight); + expect(wrapper.find('mark').first().text()).toEqual(highlight); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx index 9ad460db4c7b7..916240ac411e5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx @@ -54,7 +54,6 @@ SearchContainer.displayName = 'SearchContainer'; interface Props { filteredBrowserFields: BrowserFields; - isEventViewer?: boolean; isSearching: boolean; onOutsideClick: () => void; onSearchInputChange: (event: React.ChangeEvent) => void; @@ -93,7 +92,6 @@ CountRow.displayName = 'CountRow'; const TitleRow = React.memo<{ id: string; - isEventViewer?: boolean; onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns; }>(({ id, onOutsideClick, onUpdateColumns }) => { @@ -130,7 +128,6 @@ TitleRow.displayName = 'TitleRow'; export const Header = React.memo( ({ - isEventViewer, isSearching, filteredBrowserFields, onOutsideClick, @@ -140,12 +137,7 @@ export const Header = React.memo( timelineId, }) => ( - + = ({ columnHeaders, browserFields, height, - isEventViewer = false, onFieldSelected, onUpdateColumns, timelineId, @@ -164,7 +163,6 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ filteredBrowserFields != null ? filteredBrowserFields : browserFieldsWithDefaultCategory } height={height} - isEventViewer={isEventViewer} isSearching={isSearching} onCategorySelected={updateSelectedCategoryId} onFieldSelected={onFieldSelected} 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/manage_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx index b918e5abc652b..fe0f0c8f8b91f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx @@ -8,7 +8,6 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { getTimelineDefaults, useTimelineManager, UseTimelineManager } from './'; import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { TimelineRowAction } from '../timeline/body/actions'; const isStringifiedComparisonEqual = (a: {}, b: {}): boolean => JSON.stringify(a) === JSON.stringify(b); @@ -17,13 +16,14 @@ describe('useTimelineManager', () => { const setupMock = coreMock.createSetup(); const testId = 'coolness'; const timelineDefaults = getTimelineDefaults(testId); - const timelineRowActions = () => []; const mockFilterManager = new FilterManager(setupMock.uiSettings); + beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); }); - it('initilizes an undefined timeline', async () => { + + it('initializes an undefined timeline', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useTimelineManager() @@ -33,6 +33,7 @@ describe('useTimelineManager', () => { expect(isStringifiedComparisonEqual(uninitializedTimeline, timelineDefaults)).toBeTruthy(); }); }); + it('getIndexToAddById', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -43,6 +44,7 @@ describe('useTimelineManager', () => { expect(data).toEqual(timelineDefaults.indexToAdd); }); }); + it('setIndexToAdd', async () => { await act(async () => { const indexToAddArgs = { id: testId, indexToAdd: ['example'] }; @@ -52,13 +54,13 @@ describe('useTimelineManager', () => { await waitForNextUpdate(); result.current.initializeTimeline({ id: testId, - timelineRowActions, }); result.current.setIndexToAdd(indexToAddArgs); const data = result.current.getIndexToAddById(testId); expect(data).toEqual(indexToAddArgs.indexToAdd); }); }); + it('setIsTimelineLoading', async () => { await act(async () => { const isLoadingArgs = { id: testId, isLoading: true }; @@ -68,7 +70,6 @@ describe('useTimelineManager', () => { await waitForNextUpdate(); result.current.initializeTimeline({ id: testId, - timelineRowActions, }); let timeline = result.current.getManageTimelineById(testId); expect(timeline.isLoading).toBeFalsy(); @@ -77,29 +78,7 @@ describe('useTimelineManager', () => { expect(timeline.isLoading).toBeTruthy(); }); }); - it('setTimelineRowActions', async () => { - await act(async () => { - const timelineRowActionsEx = () => [ - { id: 'wow', content: 'hey', displayType: 'icon', onClick: () => {} } as TimelineRowAction, - ]; - const { result, waitForNextUpdate } = renderHook(() => - useTimelineManager() - ); - await waitForNextUpdate(); - result.current.initializeTimeline({ - id: testId, - timelineRowActions, - }); - let timeline = result.current.getManageTimelineById(testId); - expect(timeline.timelineRowActions).toEqual(timelineRowActions); - result.current.setTimelineRowActions({ - id: testId, - timelineRowActions: timelineRowActionsEx, - }); - timeline = result.current.getManageTimelineById(testId); - expect(timeline.timelineRowActions).toEqual(timelineRowActionsEx); - }); - }); + it('getTimelineFilterManager undefined on uninitialized', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -110,6 +89,7 @@ describe('useTimelineManager', () => { expect(data).toEqual(undefined); }); }); + it('getTimelineFilterManager defined at initialize', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -118,13 +98,13 @@ describe('useTimelineManager', () => { await waitForNextUpdate(); result.current.initializeTimeline({ id: testId, - timelineRowActions, filterManager: mockFilterManager, }); const data = result.current.getTimelineFilterManager(testId); expect(data).toEqual(mockFilterManager); }); }); + it('isManagedTimeline returns false when unset and then true when set', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => @@ -135,7 +115,6 @@ describe('useTimelineManager', () => { expect(data).toBeFalsy(); result.current.initializeTimeline({ id: testId, - timelineRowActions, filterManager: mockFilterManager, }); data = result.current.isManagedTimeline(testId); diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index 560d4c6928e4e..f82158fe65c11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -9,12 +9,10 @@ import { noop } from 'lodash/fp'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager'; -import { TimelineRowAction } from '../timeline/body/actions'; import { SubsetTimelineModel } from '../../store/timeline/model'; import * as i18n from '../../../common/components/events_viewer/translations'; import * as i18nF from '../timeline/footer/translations'; import { timelineDefaults as timelineDefaultModel } from '../../store/timeline/defaults'; -import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; interface ManageTimelineInit { documentType?: string; @@ -25,16 +23,11 @@ interface ManageTimelineInit { indexToAdd?: string[] | null; loadingText?: string; selectAll?: boolean; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; + queryFields?: string[]; title?: string; unit?: (totalCount: number) => string; } -export interface TimelineRowActionArgs { - ecsData: Ecs; - nonEcsData: TimelineNonEcsData[]; -} - interface ManageTimeline { documentType: string; defaultModel: SubsetTimelineModel; @@ -46,7 +39,6 @@ interface ManageTimeline { loadingText: string; queryFields: string[]; selectAll: boolean; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; title: string; unit: (totalCount: number) => string; } @@ -75,14 +67,6 @@ type ActionManageTimeline = type: 'SET_SELECT_ALL'; id: string; payload: boolean; - } - | { - type: 'SET_TIMELINE_ACTIONS'; - id: string; - payload: { - queryFields?: string[]; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; - }; }; export const getTimelineDefaults = (id: string) => ({ @@ -95,7 +79,6 @@ export const getTimelineDefaults = (id: string) => ({ id, isLoading: false, queryFields: [], - timelineRowActions: () => [], title: i18n.EVENTS, unit: (n: number) => i18n.UNIT(n), }); @@ -129,14 +112,7 @@ const reducerManageTimeline = ( selectAll: action.payload, }, } as ManageTimelineById; - case 'SET_TIMELINE_ACTIONS': - return { - ...state, - [action.id]: { - ...state[action.id], - ...action.payload, - }, - } as ManageTimelineById; + case 'SET_IS_LOADING': return { ...state, @@ -159,11 +135,6 @@ export interface UseTimelineManager { setIndexToAdd: (indexToAddArgs: { id: string; indexToAdd: string[] }) => void; setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void; setSelectAll: (selectAllArgs: { id: string; selectAll: boolean }) => void; - setTimelineRowActions: (actionsArgs: { - id: string; - queryFields?: string[]; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; - }) => void; } export const useTimelineManager = ( @@ -181,25 +152,6 @@ export const useTimelineManager = ( }); }, []); - const setTimelineRowActions = useCallback( - ({ - id, - queryFields, - timelineRowActions, - }: { - id: string; - queryFields?: string[]; - timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[]; - }) => { - dispatch({ - type: 'SET_TIMELINE_ACTIONS', - id, - payload: { queryFields, timelineRowActions }, - }); - }, - [] - ); - const setIsTimelineLoading = useCallback( ({ id, isLoading }: { id: string; isLoading: boolean }) => { dispatch({ @@ -236,7 +188,7 @@ export const useTimelineManager = ( if (state[id] != null) { return state[id]; } - initializeTimeline({ id, timelineRowActions: () => [] }); + initializeTimeline({ id }); return getTimelineDefaults(id); }, [initializeTimeline, state] @@ -261,7 +213,6 @@ export const useTimelineManager = ( setIndexToAdd, setIsTimelineLoading, setSelectAll, - setTimelineRowActions, }; }; @@ -274,7 +225,6 @@ const init = { setIndexToAdd: () => undefined, setIsTimelineLoading: () => noop, setSelectAll: () => noop, - setTimelineRowActions: () => noop, }; const ManageTimelineContext = createContext(init); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap index 6d2f35750d8b1..9c0a9b26f5332 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap @@ -24,6 +24,7 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "size": "64px", }, }, + "browserDefaultFontSize": "16px", "euiAnimSlightBounce": "cubic-bezier(0.34, 1.61, 0.7, 1)", "euiAnimSlightResistance": "cubic-bezier(0.694, 0.0482, 0.335, 1)", "euiAnimSpeedExtraFast": "90ms", @@ -434,6 +435,28 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "euiScrollBarCorner": "6px", "euiSelectableListItemBorder": "1px solid #202128", "euiSelectableListItemPadding": "4px 12px", + "euiSelectableTemplateSitewideTypes": Object { + "application": Object { + "color": "#6092c0", + "font-weight": 500, + }, + "article": Object { + "color": "#9777bc", + "font-weight": 500, + }, + "case": Object { + "color": "#e7664c", + "font-weight": 500, + }, + "deployment": Object { + "color": "#54b399", + "font-weight": 500, + }, + "platform": Object { + "color": "#d6bf57", + "font-weight": 500, + }, + }, "euiShadowColor": "#000000", "euiShadowColorLarge": "#000000", "euiSize": "16px", diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index ac6c61b33b35e..ed44fc14e3efa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -36,7 +36,7 @@ import { KueryFilterQueryKind } from '../../../common/store/model'; import { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../../common/store/inputs/actions'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); @@ -942,7 +942,7 @@ describe('helpers', () => { test('it invokes date range picker dispatch', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -958,7 +958,7 @@ describe('helpers', () => { test('it invokes add timeline dispatch', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -966,7 +966,7 @@ describe('helpers', () => { })(); expect(dispatchAddTimeline).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, savedTimeline: true, timeline: mockTimelineModel, }); @@ -975,7 +975,7 @@ describe('helpers', () => { test('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -989,7 +989,7 @@ describe('helpers', () => { test('it does not invoke notes dispatch if duplicate is true', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1012,7 +1012,7 @@ describe('helpers', () => { }; timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1036,7 +1036,7 @@ describe('helpers', () => { }; timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1044,14 +1044,14 @@ describe('helpers', () => { })(); expect(dispatchSetKqlFilterQueryDraft).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, filterQueryDraft: { kind: 'kuery', expression: 'expression', }, }); expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ - id: 'timeline-1', + id: TimelineId.active, filterQuery: { kuery: { kind: 'kuery', @@ -1065,7 +1065,7 @@ describe('helpers', () => { test('it invokes dispatchAddNotes if duplicate is false', () => { timelineDispatch({ duplicate: false, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [ @@ -1099,7 +1099,7 @@ describe('helpers', () => { test('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', () => { timelineDispatch({ duplicate: true, - id: 'timeline-1', + id: TimelineId.active, from: '2020-03-26T14:35:56.356Z', to: '2020-03-26T14:41:56.356Z', notes: [], @@ -1119,7 +1119,7 @@ describe('helpers', () => { expect(dispatchAddNotes).not.toHaveBeenCalled(); expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ - id: 'timeline-1', + id: TimelineId.active, noteId: 'uuid.v4()', }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index c2e23cc19d89e..b6b6148340a4a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -22,7 +22,12 @@ import { DataProviderResult, } from '../../../graphql/types'; -import { DataProviderType, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { + DataProviderType, + TimelineId, + TimelineStatus, + TimelineType, +} from '../../../../common/types/timeline'; import { addNotes as dispatchAddNotes, @@ -315,7 +320,7 @@ export const queryTimelineById = ({ updateIsLoading, updateTimeline, }: QueryTimelineById) => { - updateIsLoading({ id: 'timeline-1', isLoading: true }); + updateIsLoading({ id: TimelineId.active, isLoading: true }); if (apolloClient) { apolloClient .query({ @@ -343,7 +348,7 @@ export const queryTimelineById = ({ updateTimeline({ duplicate, from, - id: 'timeline-1', + id: TimelineId.active, notes, timeline: { ...timeline, @@ -355,7 +360,7 @@ export const queryTimelineById = ({ } }) .finally(() => { - updateIsLoading({ id: 'timeline-1', isLoading: false }); + updateIsLoading({ id: TimelineId.active, isLoading: false }); }); } }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 4c5db80a6c916..f681043a9047d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -11,6 +11,7 @@ import { Dispatch } from 'redux'; import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types'; import { State } from '../../../common/store'; +import { TimelineId } from '../../../../common/types/timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -192,7 +193,7 @@ export const StatefulOpenTimelineComponent = React.memo( const deleteTimelines: DeleteTimelines = useCallback( async (timelineIds: string[]) => { if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); + createNewTimeline({ id: TimelineId.active, columns: defaultHeaders, show: false }); } await apolloClient.mutate< @@ -369,7 +370,7 @@ export const StatefulOpenTimelineComponent = React.memo( const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const mapStateToProps = (state: State) => { - const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; + const timeline = getTimeline(state, TimelineId.active) ?? timelineDefaults; return { timeline, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 1f5f0ccca3b70..e9ae66703f017 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -224,7 +224,7 @@ export const OpenTimeline = React.memo( popoverContent={getBatchItemsPopoverContent} data-test-subj="utility-bar-action" > - {i18n.BATCH_ACTIONS} + {i18n.BATCH_ACTIONS} )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 2792b264ba7e2..d01e8634a489f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -102,12 +102,10 @@ const StatefulRowRenderersBrowserComponent: React.FC setShow(false), []); const handleDisableAll = useCallback(() => { - // eslint-disable-next-line no-unused-expressions tableRef?.current?.setSelection([]); }, [tableRef]); const handleEnableAll = useCallback(() => { - // eslint-disable-next-line no-unused-expressions tableRef?.current?.setSelection(renderers); }, [tableRef]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index 7baa7c42fb45e..f1414724e243f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -88,7 +88,7 @@ const RowRenderersBrowserComponent = React.forwardRef( (item: RowRendererOption) => () => { const newSelection = xor([item], notExcludedRowRenderers); // @ts-expect-error - ref?.current?.setSelection(newSelection); // eslint-disable-line no-unused-expressions + ref?.current?.setSelection(newSelection); }, [notExcludedRowRenderers, ref] ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx new file mode 100644 index 0000000000000..64f8ce3727f39 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx @@ -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 React, { MouseEvent } from 'react'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +import { EventsTd, EventsTdContent } from '../../styles'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; + +interface ActionIconItemProps { + ariaLabel?: string; + id: string; + width?: number; + dataTestSubj?: string; + content?: string; + iconType?: string; + isDisabled?: boolean; + onClick?: (event: MouseEvent) => void; + children?: React.ReactNode; +} + +const ActionIconItemComponent: React.FC = ({ + id, + width = DEFAULT_ICON_BUTTON_WIDTH, + dataTestSubj, + content, + ariaLabel, + iconType, + isDisabled = false, + onClick, + children, +}) => ( + + + {children ?? ( + + + + )} + + +); + +ActionIconItemComponent.displayName = 'ActionIconItemComponent'; + +export const ActionIconItem = React.memo(ActionIconItemComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx new file mode 100644 index 0000000000000..a82821675d956 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx @@ -0,0 +1,59 @@ +/* + * 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 { TimelineType, TimelineStatus } from '../../../../../../common/types/timeline'; +import { AssociateNote, UpdateNote } from '../../../notes/helpers'; +import * as i18n from '../translations'; +import { NotesButton } from '../../properties/helpers'; +import { Note } from '../../../../../common/lib/note'; +import { ActionIconItem } from './action_icon_item'; + +interface AddEventNoteActionProps { + associateNote: AssociateNote; + getNotesByIds: (noteIds: string[]) => Note[]; + noteIds: string[]; + showNotes: boolean; + status: TimelineStatus; + timelineType: TimelineType; + toggleShowNotes: () => void; + updateNote: UpdateNote; +} + +const AddEventNoteActionComponent: React.FC = ({ + associateNote, + getNotesByIds, + noteIds, + showNotes, + status, + timelineType, + toggleShowNotes, + updateNote, +}) => ( + + + +); + +AddEventNoteActionComponent.displayName = 'AddEventNoteActionComponent'; + +export const AddEventNoteAction = React.memo(AddEventNoteActionComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 78ee9bdd053b2..fb1709df01320 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -9,10 +9,7 @@ import { useSelector } from 'react-redux'; import { TestProviders, mockTimelineModel } from '../../../../../common/mock'; import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; -import * as i18n from '../translations'; - import { Actions } from '.'; -import { TimelineType } from '../../../../../../common/types/timeline'; jest.mock('react-redux', () => { const origin = jest.requireActual('react-redux'); @@ -30,22 +27,14 @@ describe('Actions', () => { ); @@ -58,22 +47,14 @@ describe('Actions', () => { ); @@ -86,22 +67,14 @@ describe('Actions', () => { ); @@ -116,22 +89,14 @@ describe('Actions', () => { ); @@ -140,197 +105,4 @@ describe('Actions', () => { expect(onEventToggled).toBeCalled(); }); - - test('it does NOT render a notes button when isEventsViewer is true', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-notes-button-small"]').exists()).toBe(false); - }); - - test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="timeline-notes-button-small"]').first().simulate('click'); - - expect(toggleShowNotes).toBeCalled(); - }); - - test('it renders correct tooltip for NotesButton - timeline', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual(i18n.NOTES_TOOLTIP); - }); - - test('it renders correct tooltip for NotesButton - timeline template', () => { - (useSelector as jest.Mock).mockReturnValue({ - ...mockTimelineModel, - timelineType: TimelineType.template, - }); - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual( - i18n.NOTES_DISABLE_TOOLTIP - ); - (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); - }); - - test('it does NOT render a pin button when isEventViewer is true', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); - }); - - test('it invokes onPinClicked when the button for pinning events is clicked', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="pin"]').first().simulate('click'); - - expect(onPinClicked).toHaveBeenCalled(); - }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index c9c8250922161..3d08d56d6fb19 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -3,203 +3,90 @@ * 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 { useSelector } from 'react-redux'; -import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { EuiButtonIcon, EuiLoadingSpinner, EuiCheckbox } from '@elastic/eui'; -import { Note } from '../../../../../common/lib/note'; -import { StoreState } from '../../../../../common/store/types'; -import { TimelineType } from '../../../../../../common/types/timeline'; - -import { TimelineModel } from '../../../../store/timeline/model'; - -import { AssociateNote, UpdateNote } from '../../../notes/helpers'; -import { Pin } from '../../pin'; -import { NotesButton } from '../../properties/helpers'; import { EventsLoading, EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles'; -import { eventHasNotes, getPinTooltip } from '../helpers'; import * as i18n from '../translations'; import { OnRowSelected } from '../../events'; -import { Ecs, TimelineNonEcsData } from '../../../../../graphql/types'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; -export interface TimelineRowActionOnClick { - eventId: string; - ecsData: Ecs; - data: TimelineNonEcsData[]; -} - -export interface TimelineRowAction { - ariaLabel?: string; - dataTestSubj?: string; - displayType: 'icon' | 'contextMenu'; - iconType?: string; - id: string; - isActionDisabled?: (ecsData?: Ecs) => boolean; - onClick: ({ eventId, ecsData }: TimelineRowActionOnClick) => void; - content: string | JSX.Element; - width?: number; -} - interface Props { actionsColumnWidth: number; additionalActions?: JSX.Element[]; - associateNote: AssociateNote; checked: boolean; onRowSelected: OnRowSelected; expanded: boolean; eventId: string; - eventIsPinned: boolean; - getNotesByIds: (noteIds: string[]) => Note[]; - isEventViewer?: boolean; loading: boolean; loadingEventIds: Readonly; - noteIds: string[]; onEventToggled: () => void; - onPinClicked: () => void; - showNotes: boolean; showCheckboxes: boolean; - toggleShowNotes: () => void; - updateNote: UpdateNote; } -const emptyNotes: string[] = []; - -export const Actions = React.memo( - ({ - actionsColumnWidth, - additionalActions, - associateNote, - checked, - expanded, - eventId, - eventIsPinned, - getNotesByIds, - isEventViewer = false, - loading = false, - loadingEventIds, - noteIds, - onEventToggled, - onPinClicked, - onRowSelected, - showCheckboxes, - showNotes, - toggleShowNotes, - updateNote, - }) => { - const timeline = useSelector((state) => { - return state.timeline.timelineById['timeline-1']; - }); - return ( - - {showCheckboxes && ( - - - {loadingEventIds.includes(eventId) ? ( - - ) : ( - ) => { - onRowSelected({ - eventIds: [eventId], - isSelected: event.currentTarget.checked, - }); - }} - /> - )} - - - )} +const ActionsComponent: React.FC = ({ + actionsColumnWidth, + additionalActions, + checked, + expanded, + eventId, + loading = false, + loadingEventIds, + onEventToggled, + onRowSelected, + showCheckboxes, +}) => { + const handleSelectEvent = useCallback( + (event: React.ChangeEvent) => + onRowSelected({ + eventIds: [eventId], + isSelected: event.currentTarget.checked, + }), + [eventId, onRowSelected] + ); - + return ( + + {showCheckboxes && ( + - {loading ? ( - + {loadingEventIds.includes(eventId) ? ( + ) : ( - )} + )} + + + {loading ? ( + + ) : ( + + )} + + - <>{additionalActions} + <>{additionalActions} + + ); +}; - {!isEventViewer && ( - <> - - - - - - - +ActionsComponent.displayName = 'ActionsComponent'; - - - - - - - )} - - ); - }, - (nextProps, prevProps) => { - return ( - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.additionalActions === nextProps.additionalActions && - prevProps.checked === nextProps.checked && - prevProps.expanded === nextProps.expanded && - prevProps.eventId === nextProps.eventId && - prevProps.eventIsPinned === nextProps.eventIsPinned && - prevProps.loading === nextProps.loading && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.noteIds === nextProps.noteIds && - prevProps.onRowSelected === nextProps.onRowSelected && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showNotes === nextProps.showNotes - ); - } -); -Actions.displayName = 'Actions'; +export const Actions = React.memo(ActionsComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx new file mode 100644 index 0000000000000..2f9f15938cad6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx @@ -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 React, { useMemo } from 'react'; +import { EuiToolTip } from '@elastic/eui'; + +import { EventsTd, EventsTdContent } from '../../styles'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; +import { eventHasNotes, getPinTooltip } from '../helpers'; +import { Pin } from '../../pin'; +import { TimelineType } from '../../../../../../common/types/timeline'; + +interface PinEventActionProps { + noteIds: string[]; + onPinClicked: () => void; + eventIsPinned: boolean; + timelineType: TimelineType; +} + +const PinEventActionComponent: React.FC = ({ + noteIds, + onPinClicked, + eventIsPinned, + timelineType, +}) => { + const tooltipContent = useMemo( + () => + getPinTooltip({ + isPinned: eventIsPinned, + eventHasNotes: eventHasNotes(noteIds), + timelineType, + }), + [eventIsPinned, noteIds, timelineType] + ); + + return ( + + + + + + + + ); +}; + +PinEventActionComponent.displayName = 'PinEventActionComponent'; + +export const PinEventAction = React.memo(PinEventActionComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index a3e177604fbd4..120fc12b425f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -235,7 +235,6 @@ export const ColumnHeadersComponent = ({ columnHeaders={columnHeaders} data-test-subj="field-browser" height={FIELD_BROWSER_HEIGHT} - isEventViewer={isEventViewer} onUpdateColumns={onUpdateColumns} timelineId={timelineId} toggleColumn={toggleColumn} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx new file mode 100644 index 0000000000000..ae552ade665cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { mount } from 'enzyme'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { TestProviders, mockTimelineModel } from '../../../../../common/mock'; +import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; +import * as i18n from '../translations'; + +import { EventColumnView } from './event_column_view'; +import { TimelineType } from '../../../../../../common/types/timeline'; + +jest.mock('react-redux', () => { + const origin = jest.requireActual('react-redux'); + return { + ...origin, + useSelector: jest.fn(), + }; +}); + +describe('EventColumnView', () => { + (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); + + const props = { + id: 'event-id', + actionsColumnWidth: DEFAULT_ACTIONS_COLUMN_WIDTH, + associateNote: jest.fn(), + columnHeaders: [], + columnRenderers: [], + data: [ + { + field: 'host.name', + }, + ], + ecsData: { + _id: 'id', + }, + eventIdToNoteIds: {}, + expanded: false, + getNotesByIds: jest.fn(), + loading: false, + loadingEventIds: [], + onColumnResized: jest.fn(), + onEventToggled: jest.fn(), + onPinEvent: jest.fn(), + onRowSelected: jest.fn(), + onUnPinEvent: jest.fn(), + refetch: jest.fn(), + selectedEventIds: {}, + showCheckboxes: false, + showNotes: false, + timelineId: 'timeline-1', + toggleShowNotes: jest.fn(), + updateNote: jest.fn(), + isEventPinned: false, + }; + + test('it does NOT render a notes button when isEventsViewer is true', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('[data-test-subj="timeline-notes-button-small"]').exists()).toBe(false); + }); + + test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(props.toggleShowNotes).not.toHaveBeenCalled(); + + wrapper.find('[data-test-subj="timeline-notes-button-small"]').first().simulate('click'); + + expect(props.toggleShowNotes).toHaveBeenCalled(); + }); + + test('it renders correct tooltip for NotesButton - timeline', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual(i18n.NOTES_TOOLTIP); + }); + + test('it renders correct tooltip for NotesButton - timeline template', () => { + (useSelector as jest.Mock).mockReturnValue({ + ...mockTimelineModel, + timelineType: TimelineType.template, + }); + + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual( + i18n.NOTES_DISABLE_TOOLTIP + ); + (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); + }); + + test('it does NOT render a pin button when isEventViewer is true', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); + }); + + test('it invokes onPinClicked when the button for pinning events is clicked', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect(props.onPinEvent).not.toHaveBeenCalled(); + + wrapper.find('[data-test-subj="pin"]').first().simulate('click'); + + expect(props.onPinEvent).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index e7462188001e9..f1d45d5458554 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -4,29 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import uuid from 'uuid'; +import { useSelector, shallowEqual } from 'react-redux'; -import { - EuiButtonIcon, - EuiToolTip, - EuiContextMenuPanel, - EuiPopover, - EuiContextMenuItem, -} from '@elastic/eui'; -import styled from 'styled-components'; import { TimelineNonEcsData, Ecs } from '../../../../../graphql/types'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { AssociateNote, UpdateNote } from '../../../notes/helpers'; import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; -import { EventsTd, EventsTdContent, EventsTrData } from '../../styles'; +import { EventsTrData } from '../../styles'; import { Actions } from '../actions'; import { DataDrivenColumns } from '../data_driven_columns'; -import { eventHasNotes, getPinOnClick } from '../helpers'; +import { + eventHasNotes, + getEventType, + getPinOnClick, + InvestigateInResolverAction, +} from '../helpers'; import { ColumnRenderer } from '../renderers/column_renderer'; -import { useManageTimeline } from '../../../manage_timeline'; +import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; +import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; +import { AddEventNoteAction } from '../actions/add_note_icon_item'; +import { PinEventAction } from '../actions/pin_event_action'; +import { StoreState } from '../../../../../common/store/types'; +import { inputsModel } from '../../../../../common/store'; +import { TimelineId } from '../../../../../../common/types/timeline'; + +import { TimelineModel } from '../../../../store/timeline/model'; interface Props { id: string; @@ -48,6 +53,7 @@ interface Props { onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; + refetch: inputsModel.Refetch; selectedEventIds: Readonly>; showCheckboxes: boolean; showNotes: boolean; @@ -81,6 +87,7 @@ export const EventColumnView = React.memo( onPinEvent, onRowSelected, onUnPinEvent, + refetch, selectedEventIds, showCheckboxes, showNotes, @@ -88,114 +95,10 @@ export const EventColumnView = React.memo( toggleShowNotes, updateNote, }) => { - const { getManageTimelineById } = useManageTimeline(); - const timelineActions = useMemo( - () => getManageTimelineById(timelineId).timelineRowActions({ nonEcsData: data, ecsData }), - [data, ecsData, getManageTimelineById, timelineId] + const { timelineType, status } = useSelector( + (state) => state.timeline.timelineById[timelineId], + shallowEqual ); - const [isPopoverOpen, setPopover] = useState(false); - - const onButtonClick = useCallback(() => { - setPopover(!isPopoverOpen); - }, [isPopoverOpen]); - - const closePopover = useCallback(() => { - setPopover(false); - }, []); - - const button = ( - - ); - - const onClickCb = useCallback((cb: () => void) => { - cb(); - closePopover(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const additionalActions = useMemo(() => { - const grouped = timelineActions.reduce( - ( - acc: { - contextMenu: JSX.Element[]; - icon: JSX.Element[]; - }, - action - ) => { - if (action.displayType === 'icon') { - return { - ...acc, - icon: [ - ...acc.icon, - - - - action.onClick({ eventId: id, ecsData, data })} - /> - - - , - ], - }; - } - return { - ...acc, - contextMenu: [ - ...acc.contextMenu, - onClickCb(() => action.onClick({ eventId: id, ecsData, data }))} - > - {action.content} - , - ], - }; - }, - { icon: [], contextMenu: [] } - ); - return grouped.contextMenu.length > 0 - ? [ - ...grouped.icon, - - - - - - - , - ] - : grouped.icon; - }, [button, closePopover, id, onClickCb, data, ecsData, timelineActions, isPopoverOpen]); const handlePinClicked = useCallback( () => @@ -209,29 +112,90 @@ export const EventColumnView = React.memo( [eventIdToNoteIds, id, isEventPinned, onPinEvent, onUnPinEvent] ); + const eventType = getEventType(ecsData); + + const additionalActions = useMemo( + () => [ + , + ...(timelineId !== TimelineId.active && eventType === 'signal' + ? [ + , + ] + : []), + ...(!isEventViewer + ? [ + , + , + ] + : []), + , + ], + [ + associateNote, + data, + ecsData, + eventIdToNoteIds, + eventType, + getNotesByIds, + handlePinClicked, + id, + isEventPinned, + isEventViewer, + refetch, + showNotes, + status, + timelineId, + timelineType, + toggleShowNotes, + updateNote, + ] + ); + return ( ( /> ); - }, - (prevProps, nextProps) => { - return ( - prevProps.id === nextProps.id && - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.columnHeaders === nextProps.columnHeaders && - prevProps.columnRenderers === nextProps.columnRenderers && - prevProps.data === nextProps.data && - prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.expanded === nextProps.expanded && - prevProps.loading === nextProps.loading && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.isEventPinned === nextProps.isEventPinned && - prevProps.onRowSelected === nextProps.onRowSelected && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showNotes === nextProps.showNotes && - prevProps.timelineId === nextProps.timelineId - ); } ); -const ContextMenuPanel = styled(EuiContextMenuPanel)` - font-size: ${({ theme }) => theme.eui.euiFontSizeS}; -`; -ContextMenuPanel.displayName = 'ContextMenuPanel'; +EventColumnView.displayName = 'EventColumnView'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index ca7a64db58c95..64d55f8cf6c6a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import { inputsModel } from '../../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../../graphql/types'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; @@ -44,6 +45,7 @@ interface Props { onUpdateColumns: OnUpdateColumns; onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; + refetch: inputsModel.Refetch; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -71,6 +73,7 @@ const EventsComponent: React.FC = ({ onUpdateColumns, onUnPinEvent, pinnedEventIds, + refetch, rowRenderers, selectedEventIds, showCheckboxes, @@ -78,7 +81,7 @@ const EventsComponent: React.FC = ({ updateNote, }) => ( - {data.map((event, i) => ( + {data.map((event) => ( = ({ onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} + refetch={refetch} rowRenderers={rowRenderers} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} 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 3236482e6bc27..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 @@ -9,9 +9,11 @@ import { useSelector } from 'react-redux'; import uuid from 'uuid'; 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'; @@ -33,7 +35,7 @@ import { getEventType } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; import { EventColumnView } from './event_column_view'; -import { StoreState } from '../../../../../common/store'; +import { inputsModel, StoreState } from '../../../../../common/store'; interface Props { actionsColumnWidth: number; @@ -55,6 +57,7 @@ interface Props { onUnPinEvent: OnUnPinEvent; onUpdateColumns: OnUpdateColumns; isEventPinned: boolean; + refetch: inputsModel.Refetch; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -121,6 +124,7 @@ const StatefulEventComponent: React.FC = ({ onRowSelected, onUnPinEvent, onUpdateColumns, + refetch, rowRenderers, selectedEventIds, showCheckboxes, @@ -130,10 +134,16 @@ const StatefulEventComponent: React.FC = ({ }) => { const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({}); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); - const timeline = useSelector((state) => { - return state.timeline.timelineById['timeline-1']; - }); + const { status: timelineStatus } = useSelector( + (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; @@ -171,93 +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/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts deleted file mode 100644 index b62888fbf8427..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, isEmpty } from 'lodash/fp'; -import { Dispatch } from 'redux'; - -import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; -import { updateTimelineGraphEventId } from '../../../store/timeline/actions'; -import { EventType } from '../../../../timelines/store/timeline/model'; -import { OnPinEvent, OnUnPinEvent } from '../events'; - -import { TimelineRowAction, TimelineRowActionOnClick } from './actions'; - -import * as i18n from './translations'; -import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => - k !== '__typename' && v != null ? v : undefined; - -export const stringifyEvent = (ecs: Ecs): string => JSON.stringify(ecs, omitTypenameAndEmpty, 2); - -export const eventHasNotes = (noteIds: string[]): boolean => !isEmpty(noteIds); - -export const getPinTooltip = ({ - isPinned, - // eslint-disable-next-line no-shadow - eventHasNotes, - timelineType, -}: { - isPinned: boolean; - eventHasNotes: boolean; - timelineType: TimelineTypeLiteral; -}) => - timelineType === TimelineType.template - ? i18n.DISABLE_PIN - : isPinned && eventHasNotes - ? i18n.PINNED_WITH_NOTES - : isPinned - ? i18n.PINNED - : i18n.UNPINNED; - -export interface IsPinnedParams { - eventId: string; - pinnedEventIds: Readonly>; -} - -export const eventIsPinned = ({ eventId, pinnedEventIds }: IsPinnedParams): boolean => - pinnedEventIds[eventId] === true; - -export interface GetPinOnClickParams { - allowUnpinning: boolean; - eventId: string; - onPinEvent: OnPinEvent; - onUnPinEvent: OnUnPinEvent; - isEventPinned: boolean; -} - -export const getPinOnClick = ({ - allowUnpinning, - eventId, - onPinEvent, - onUnPinEvent, - isEventPinned, -}: GetPinOnClickParams) => { - if (!allowUnpinning) { - return; - } - - if (isEventPinned) { - onUnPinEvent(eventId); - } else { - onPinEvent(eventId); - } -}; - -/** - * Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field - * data necessary for custom timeline actions in conjunction with selection state - * @param timelineData - * @param eventIds - * @param fieldsToKeep - */ -export const getEventIdToDataMapping = ( - timelineData: TimelineItem[], - eventIds: string[], - fieldsToKeep: string[] -): Record => { - return timelineData.reduce((acc, v) => { - const fvm = eventIds.includes(v._id) - ? { [v._id]: v.data.filter((ti) => fieldsToKeep.includes(ti.field)) } - : {}; - return { - ...acc, - ...fvm, - }; - }, {}); -}; - -/** Return eventType raw or signal */ -export const getEventType = (event: Ecs): Omit => { - if (!isEmpty(event.signal?.rule?.id)) { - return 'signal'; - } - return 'raw'; -}; - -export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => { - return ( - get(['agent', 'type', 0], ecsData) === 'endpoint' && - get(['process', 'entity_id'], ecsData)?.length === 1 && - get(['process', 'entity_id', 0], ecsData) !== '' - ); -}; - -export const getInvestigateInResolverAction = ({ - dispatch, - timelineId, -}: { - dispatch: Dispatch; - timelineId: string; -}): TimelineRowAction => ({ - ariaLabel: i18n.ACTION_INVESTIGATE_IN_RESOLVER, - content: i18n.ACTION_INVESTIGATE_IN_RESOLVER, - dataTestSubj: 'investigate-in-resolver', - displayType: 'icon', - iconType: 'node', - id: 'investigateInResolver', - isActionDisabled: (ecsData?: Ecs) => !isInvestigateInResolverActionEnabled(ecsData), - onClick: ({ eventId }: TimelineRowActionOnClick) => - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: eventId })), - width: DEFAULT_ICON_BUTTON_WIDTH, -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx new file mode 100644 index 0000000000000..5753efa2bf1bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -0,0 +1,146 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { get, isEmpty } from 'lodash/fp'; +import { useDispatch } from 'react-redux'; + +import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; +import { updateTimelineGraphEventId } from '../../../store/timeline/actions'; +import { EventType } from '../../../store/timeline/model'; +import { OnPinEvent, OnUnPinEvent } from '../events'; +import { ActionIconItem } from './actions/action_icon_item'; + +import * as i18n from './translations'; +import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => + k !== '__typename' && v != null ? v : undefined; + +export const stringifyEvent = (ecs: Ecs): string => JSON.stringify(ecs, omitTypenameAndEmpty, 2); + +export const eventHasNotes = (noteIds: string[]): boolean => !isEmpty(noteIds); + +export const getPinTooltip = ({ + isPinned, + // eslint-disable-next-line no-shadow + eventHasNotes, + timelineType, +}: { + isPinned: boolean; + eventHasNotes: boolean; + timelineType: TimelineTypeLiteral; +}) => + timelineType === TimelineType.template + ? i18n.DISABLE_PIN + : isPinned && eventHasNotes + ? i18n.PINNED_WITH_NOTES + : isPinned + ? i18n.PINNED + : i18n.UNPINNED; + +export interface IsPinnedParams { + eventId: string; + pinnedEventIds: Readonly>; +} + +export const eventIsPinned = ({ eventId, pinnedEventIds }: IsPinnedParams): boolean => + pinnedEventIds[eventId] === true; + +export interface GetPinOnClickParams { + allowUnpinning: boolean; + eventId: string; + onPinEvent: OnPinEvent; + onUnPinEvent: OnUnPinEvent; + isEventPinned: boolean; +} + +export const getPinOnClick = ({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent, + isEventPinned, +}: GetPinOnClickParams) => { + if (!allowUnpinning) { + return; + } + + if (isEventPinned) { + onUnPinEvent(eventId); + } else { + onPinEvent(eventId); + } +}; + +/** + * Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field + * data necessary for custom timeline actions in conjunction with selection state + * @param timelineData + * @param eventIds + * @param fieldsToKeep + */ +export const getEventIdToDataMapping = ( + timelineData: TimelineItem[], + eventIds: string[], + fieldsToKeep: string[] +): Record => + timelineData.reduce((acc, v) => { + const fvm = eventIds.includes(v._id) + ? { [v._id]: v.data.filter((ti) => fieldsToKeep.includes(ti.field)) } + : {}; + return { + ...acc, + ...fvm, + }; + }, {}); + +/** Return eventType raw or signal */ +export const getEventType = (event: Ecs): Omit => { + if (!isEmpty(event.signal?.rule?.id)) { + return 'signal'; + } + return 'raw'; +}; + +export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => + get(['agent', 'type', 0], ecsData) === 'endpoint' && + get(['process', 'entity_id'], ecsData)?.length === 1 && + get(['process', 'entity_id', 0], ecsData) !== ''; + +interface InvestigateInResolverActionProps { + timelineId: string; + ecsData: Ecs; +} + +const InvestigateInResolverActionComponent: React.FC = ({ + timelineId, + ecsData, +}) => { + const dispatch = useDispatch(); + const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); + const handleClick = useCallback( + () => dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })), + [dispatch, ecsData._id, timelineId] + ); + + return ( + + ); +}; + +InvestigateInResolverActionComponent.displayName = 'InvestigateInResolverActionComponent'; + +export const InvestigateInResolverAction = React.memo(InvestigateInResolverActionComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 4eac5360321c1..657e1617e8d24 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -64,7 +64,6 @@ describe('Body', () => { data: mockTimelineData, docValueFields: [], eventIdToNoteIds: {}, - id: 'timeline-test', isSelectAllChecked: false, getNotesByIds: mockGetNotesByIds, loadingEventIds: [], @@ -78,11 +77,13 @@ describe('Body', () => { onUnPinEvent: jest.fn(), onUpdateColumns: jest.fn(), pinnedEventIds: {}, + refetch: jest.fn(), rowRenderers, selectedEventIds: {}, show: true, sort: mockSort, showCheckboxes: false, + timelineId: 'timeline-test', timelineType: TimelineType.default, toggleColumn: jest.fn(), updateNote: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 6f578ffe3e956..40cc12afde51d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -6,10 +6,11 @@ import React, { useMemo, useRef } from 'react'; +import { inputsModel } from '../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions, EventType } from '../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; import { OnColumnRemoved, @@ -42,10 +43,10 @@ export interface BodyProps { docValueFields: DocValueFields[]; getNotesByIds: (noteIds: string[]) => Note[]; graphEventId?: string; - id: string; isEventViewer?: boolean; isSelectAllChecked: boolean; eventIdToNoteIds: Readonly>; + eventType?: EventType; loadingEventIds: Readonly; onColumnRemoved: OnColumnRemoved; onColumnResized: OnColumnResized; @@ -57,18 +58,23 @@ export interface BodyProps { onUpdateColumns: OnUpdateColumns; onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; + refetch: inputsModel.Refetch; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; show: boolean; showCheckboxes: boolean; sort: Sort; + timelineId: string; timelineType: TimelineType; toggleColumn: (column: ColumnHeaderOptions) => void; updateNote: UpdateNote; } -export const hasAdditonalActions = (id: string): boolean => - id === TimelineId.detectionsPage || id === TimelineId.detectionsRulesDetailsPage; +export const hasAdditionalActions = (id: string, eventType?: EventType): boolean => + id === TimelineId.detectionsPage || + id === TimelineId.detectionsRulesDetailsPage || + ((id === TimelineId.active && eventType && ['all', 'signal', 'alert'].includes(eventType)) ?? + false); const EXTRA_WIDTH = 4; // px @@ -82,9 +88,9 @@ export const Body = React.memo( data, docValueFields, eventIdToNoteIds, + eventType, getNotesByIds, graphEventId, - id, isEventViewer = false, isSelectAllChecked, loadingEventIds, @@ -99,11 +105,13 @@ export const Body = React.memo( onUnPinEvent, pinnedEventIds, rowRenderers, + refetch, selectedEventIds, show, showCheckboxes, sort, toggleColumn, + timelineId, timelineType, updateNote, }) => { @@ -113,9 +121,9 @@ export const Body = React.memo( getActionsColumnWidth( isEventViewer, showCheckboxes, - hasAdditonalActions(id) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0 + hasAdditionalActions(timelineId, eventType) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0 ), - [isEventViewer, showCheckboxes, id] + [isEventViewer, showCheckboxes, timelineId, eventType] ); const columnWidths = useMemo( @@ -127,11 +135,15 @@ export const Body = React.memo( return ( <> {graphEventId && ( - + )} @@ -151,7 +163,7 @@ export const Body = React.memo( showEventsSelect={false} showSelectAllCheckbox={showCheckboxes} sort={sort} - timelineId={id} + timelineId={timelineId} toggleColumn={toggleColumn} /> @@ -166,7 +178,7 @@ export const Body = React.memo( docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} - id={id} + id={timelineId} isEventViewer={isEventViewer} loadingEventIds={loadingEventIds} onColumnResized={onColumnResized} @@ -175,6 +187,7 @@ export const Body = React.memo( onUpdateColumns={onUpdateColumns} onUnPinEvent={onUnPinEvent} pinnedEventIds={pinnedEventIds} + refetch={refetch} rowRenderers={rowRenderers} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 8deda03ece70e..9b7b896a2ec69 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -14,7 +14,7 @@ import { RowRendererId, TimelineId } from '../../../../../common/types/timeline' import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; -import { appSelectors, State } from '../../../../common/store'; +import { appSelectors, inputsModel, State } from '../../../../common/store'; import { appActions } from '../../../../common/store/actions'; import { useManageTimeline } from '../../manage_timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; @@ -46,6 +46,7 @@ interface OwnProps { isEventViewer?: boolean; sort: Sort; toggleColumn: (column: ColumnHeaderOptions) => void; + refetch: inputsModel.Refetch; } type StatefulBodyComponentProps = OwnProps & PropsFromRedux; @@ -61,6 +62,7 @@ const StatefulBodyComponent = React.memo( data, docValueFields, eventIdToNoteIds, + eventType, excludedRowRendererIds, id, isEventViewer = false, @@ -76,6 +78,7 @@ const StatefulBodyComponent = React.memo( show, showCheckboxes, graphEventId, + refetch, sort, timelineType, toggleColumn, @@ -195,9 +198,9 @@ const StatefulBodyComponent = React.memo( data={data} docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} + eventType={eventType} getNotesByIds={getNotesByIds} graphEventId={graphEventId} - id={id} isEventViewer={isEventViewer} isSelectAllChecked={isSelectAllChecked} loadingEventIds={loadingEventIds} @@ -211,11 +214,13 @@ const StatefulBodyComponent = React.memo( onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} pinnedEventIds={pinnedEventIds} + refetch={refetch} rowRenderers={enabledRowRenderers} selectedEventIds={selectedEventIds} show={id === TimelineId.active ? show : true} showCheckboxes={showCheckboxes} sort={sort} + timelineId={id} timelineType={timelineType} toggleColumn={toggleColumn} updateNote={onUpdateNote} @@ -229,6 +234,7 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && + prevProps.eventType === nextProps.eventType && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && prevProps.id === nextProps.id && 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/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 86334308558b8..4ab05af5dd6d4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -272,9 +272,6 @@ export interface NewTimelineProps { export const NewTimeline = React.memo( ({ closeGearMenu, outline = false, timelineId, title = i18n.NEW_TIMELINE }) => { - const uiCapabilities = useKibana().services.application.capabilities; - const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud; - const { getButton } = useCreateTimelineButton({ timelineId, timelineType: TimelineType.default, @@ -282,7 +279,7 @@ export const NewTimeline = React.memo( }); const button = getButton({ outline, title }); - return capabilitiesCanUserCRUD ? button : null; + return button; } ); NewTimeline.displayName = 'NewTimeline'; @@ -334,26 +331,23 @@ const LargeNotesButton = React.memo(({ noteIds, text, tog LargeNotesButton.displayName = 'LargeNotesButton'; interface SmallNotesButtonProps { - noteIds: string[]; toggleShowNotes: () => void; timelineType: TimelineTypeLiteral; } -const SmallNotesButton = React.memo( - ({ noteIds, toggleShowNotes, timelineType }) => { - const isTemplate = timelineType === TimelineType.template; - - return ( - toggleShowNotes()} - isDisabled={isTemplate} - /> - ); - } -); +const SmallNotesButton = React.memo(({ toggleShowNotes, timelineType }) => { + const isTemplate = timelineType === TimelineType.template; + + return ( + toggleShowNotes()} + isDisabled={isTemplate} + /> + ); +}); SmallNotesButton.displayName = 'SmallNotesButton'; /** @@ -378,11 +372,7 @@ const NotesButtonComponent = React.memo( {size === 'l' ? ( ) : ( - + )} {size === 'l' && showNotes ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx index e88ecee81d364..b5aadaa6f1ef8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { useKibana } from '../../../../common/lib/kibana'; import { useCreateTimelineButton } from './use_create_timeline'; @@ -22,7 +22,7 @@ export const NewTemplateTimelineComponent: React.FC = ({ closeGearMenu, outline, title, - timelineId = 'timeline-1', + timelineId = TimelineId.active, }) => { const uiCapabilities = useKibana().services.application.capabilities; const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx index 70257c97a6887..12eab4942128f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx @@ -22,7 +22,6 @@ import { TimelineType, } from '../../../../../common/types/timeline'; import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect'; -import { useKibana } from '../../../../common/lib/kibana'; import { Note } from '../../../../common/lib/note'; import { AssociateNote } from '../../notes/helpers'; @@ -121,8 +120,6 @@ const PropertiesRightComponent: React.FC = ({ updateNote, usersViewing, }) => { - const uiCapabilities = useKibana().services.application.capabilities; - const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud; return ( @@ -143,15 +140,13 @@ const PropertiesRightComponent: React.FC = ({ repositionOnScroll > - {capabilitiesCanUserCRUD && ( - - - - )} + + + = ({ toggleColumn, usersViewing, }) => { - const dispatch = useDispatch(); const kibana = useKibana(); const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); const esQueryConfig = useMemo(() => esQuery.getEsQueryConfig(kibana.services.uiSettings), [ @@ -213,7 +211,10 @@ export const TimelineComponent: React.FC = ({ [isLoadingSource, combinedQueries, start, end] ); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; - const timelineQueryFields = useMemo(() => columnsHeader.map((c) => c.id), [columnsHeader]); + const timelineQueryFields = useMemo(() => { + const columnFields = columnsHeader.map((c) => c.id); + return [...columnFields, ...requiredFieldsForActions]; + }, [columnsHeader]); const timelineQuerySortField = useMemo( () => ({ sortFieldId: sort.columnId, @@ -228,7 +229,6 @@ export const TimelineComponent: React.FC = ({ filterManager, id, indexToAdd, - timelineRowActions: () => [getInvestigateInResolverAction({ dispatch, timelineId: id })], }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -317,6 +317,7 @@ export const TimelineComponent: React.FC = ({ data={events} docValueFields={docValueFields} id={id} + refetch={refetch} sort={sort} toggleColumn={toggleColumn} /> diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts index 42c01da7e23c9..8ef740f7bc1d7 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts @@ -220,6 +220,109 @@ describe('persistTimeline', () => { }); }); + describe('create draft timeline in read-only permission', () => { + const timelineId = null; + const initialDraftTimeline = { + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + }, + ], + dataProviders: [], + description: 'x', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + }, + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + dateRange: { + start: 1590998565409, + end: 1591084965409, + }, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + status: TimelineStatus.draft, + }; + + const version = null; + const fetchMock = jest.fn(); + const postMock = jest.fn(); + const patchMock = jest.fn(); + + beforeAll(() => { + jest.resetAllMocks(); + jest.resetModules(); + + (KibanaServices.get as jest.Mock).mockReturnValue({ + http: { + fetch: fetchMock.mockRejectedValue({ + body: { status_code: 403, message: 'you do not have the permission' }, + }), + post: postMock.mockRejectedValue({ + body: { status_code: 403, message: 'you do not have the permission' }, + }), + patch: patchMock.mockRejectedValue({ + body: { status_code: 403, message: 'you do not have the permission' }, + }), + }, + }); + }); + + test('it should return your request timeline with code and message', async () => { + const persist = await api.persistTimeline({ + timelineId, + timeline: initialDraftTimeline, + version, + }); + expect(persist).toEqual({ + data: { + persistTimeline: { + code: 403, + message: 'you do not have the permission', + timeline: { ...initialDraftTimeline, savedObjectId: '', version: '' }, + }, + }, + }); + }); + }); + describe('create active timeline (import)', () => { const timelineId = null; const importTimeline = { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index e08d52066ebdc..c6794d125368e 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -6,6 +6,7 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; +import isEmpty from 'lodash/isEmpty'; import { throwErrors } from '../../../../case/common/api'; import { @@ -99,44 +100,63 @@ export const persistTimeline = async ({ timeline, version, }: RequestPersistTimeline): Promise => { - if (timelineId == null && timeline.status === TimelineStatus.draft && timeline) { - const draftTimeline = await cleanDraftTimeline({ - timelineType: timeline.timelineType!, - templateTimelineId: timeline.templateTimelineId ?? undefined, - templateTimelineVersion: timeline.templateTimelineVersion ?? undefined, - }); - - const templateTimelineInfo = - timeline.timelineType! === TimelineType.template - ? { - templateTimelineId: - draftTimeline.data.persistTimeline.timeline.templateTimelineId ?? - timeline.templateTimelineId, - templateTimelineVersion: - draftTimeline.data.persistTimeline.timeline.templateTimelineVersion ?? - timeline.templateTimelineVersion, - } - : {}; + try { + if (isEmpty(timelineId) && timeline.status === TimelineStatus.draft && timeline) { + const draftTimeline = await cleanDraftTimeline({ + timelineType: timeline.timelineType!, + templateTimelineId: timeline.templateTimelineId ?? undefined, + templateTimelineVersion: timeline.templateTimelineVersion ?? undefined, + }); + + const templateTimelineInfo = + timeline.timelineType! === TimelineType.template + ? { + templateTimelineId: + draftTimeline.data.persistTimeline.timeline.templateTimelineId ?? + timeline.templateTimelineId, + templateTimelineVersion: + draftTimeline.data.persistTimeline.timeline.templateTimelineVersion ?? + timeline.templateTimelineVersion, + } + : {}; + + return patchTimeline({ + timelineId: draftTimeline.data.persistTimeline.timeline.savedObjectId, + timeline: { + ...timeline, + ...templateTimelineInfo, + }, + version: draftTimeline.data.persistTimeline.timeline.version ?? '', + }); + } + + if (isEmpty(timelineId)) { + return postTimeline({ timeline }); + } return patchTimeline({ - timelineId: draftTimeline.data.persistTimeline.timeline.savedObjectId, - timeline: { - ...timeline, - ...templateTimelineInfo, - }, - version: draftTimeline.data.persistTimeline.timeline.version ?? '', + timelineId: timelineId ?? '-1', + timeline, + version: version ?? '', }); + } catch (err) { + if (err.status_code === 403 || err.body.status_code === 403) { + return Promise.resolve({ + data: { + persistTimeline: { + code: 403, + message: err.message || err.body.message, + timeline: { + ...timeline, + savedObjectId: '', + version: '', + }, + }, + }, + }); + } + return Promise.resolve(err); } - - if (timelineId == null) { - return postTimeline({ timeline }); - } - - return patchTimeline({ - timelineId, - timeline, - version: version ?? '', - }); }; export const importTimelines = async ({ 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/timelines/containers/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts index 5a162fd2206a1..c67ad45bede94 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts @@ -200,6 +200,7 @@ export const timelineQuery = gql` country_iso_code } signal { + status original_time rule { id diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index c22acf6ba7cc1..79d0f909c7d59 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; import { useParams } from 'react-router-dom'; -import { TimelineType } from '../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../common/types/timeline'; import { HeaderPage } from '../../common/components/header_page'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useKibana } from '../../common/lib/kibana'; @@ -64,13 +64,11 @@ export const TimelinesPageComponent: React.FC = () => { {tabName === TimelineType.default ? ( - {capabilitiesCanUserCRUD && ( - - )} + ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 06dd6f44bea94..8c3f30c75c35b 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -42,7 +42,7 @@ import { Direction } from '../../../graphql/types'; import { addTimelineInStorage } from '../../containers/local_storage'; import { isPageTimeline } from './epic_local_storage'; -import { TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { TimelineId, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; jest.mock('../../containers/local_storage'); @@ -115,7 +115,7 @@ describe('epicLocalStorage', () => { }); it('filters correctly page timelines', () => { - expect(isPageTimeline('timeline-1')).toBe(false); + expect(isPageTimeline(TimelineId.active)).toBe(false); expect(isPageTimeline('hosts-page-alerts')).toBe(true); }); 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/scripts/endpoint/README.md b/x-pack/plugins/security_solution/scripts/endpoint/README.md index bd9502f2f59e0..2827ab065504b 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/README.md +++ b/x-pack/plugins/security_solution/scripts/endpoint/README.md @@ -5,10 +5,6 @@ The default behavior is to create 1 endpoint with 1 alert and a moderate number A seed value can be provided as a string for the random number generator for repeatable behavior, useful for demos etc. Use the `-d` option if you want to delete and remake the indices, otherwise it will add documents to existing indices. -The sample data generator script depends on ts-node, install with npm: - -`npm install -g ts-node` - Example command sequence to get ES and kibana running with sample data after installing ts-node: `yarn es snapshot` -> starts ES diff --git a/x-pack/plugins/security_solution/scripts/endpoint/cli_tsconfig.json b/x-pack/plugins/security_solution/scripts/endpoint/cli_tsconfig.json deleted file mode 100644 index 5c68f8ee0abf2..0000000000000 --- a/x-pack/plugins/security_solution/scripts/endpoint/cli_tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../../tsconfig.json", - "compilerOptions": { - "target": "es2019", - "resolveJsonModule": true - } - } diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.js b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.js new file mode 100644 index 0000000000000..fb5a865439bc2 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.js @@ -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. + */ + +require('../../../../../src/setup_node_env'); +require('./resolver_generator_script'); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts similarity index 100% rename from x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts rename to x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts diff --git a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json index ea7a11b89dab2..ac56a6af31c72 100644 --- a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json +++ b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json @@ -10,7 +10,6 @@ "exclude": [ "test/**/*", "**/__fixtures__/**/*", - "plugins/security_solution/cypress/**/*", - "**/typespec_tests.ts" + "plugins/security_solution/cypress/**/*" ] } diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts index 73f2124736a79..c28ffcf5b7a3f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -49,6 +49,36 @@ describe('ingest_integration tests ', () => { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, }, manifest_version: '1.0.0', schema_version: 'v1', diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 7f90aa7b91063..457e50b686863 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -14,6 +14,8 @@ export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', SAVED_OBJECT_TYPE: 'endpoint:user-artifact', SUPPORTED_OPERATING_SYSTEMS: ['macos', 'windows'], + SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'], + GLOBAL_TRUSTED_APPS_NAME: 'endpoint-trustlist', }; export const ManifestConstants = { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index fea3b2b9a4526..a10ba9d6be38c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -11,6 +11,8 @@ import { getExceptionListItemSchemaMock } from '../../../../../lists/common/sche import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types'; import { buildArtifact, getFullEndpointExceptionList } from './lists'; import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts'; +import { ArtifactConstants } from './common'; +import { ENDPOINT_LIST_ID } from '../../../../../lists/common'; describe('buildEventTypeSignal', () => { let mockExceptionClient: ExceptionListClient; @@ -47,7 +49,12 @@ describe('buildEventTypeSignal', () => { const first = getFoundExceptionListItemSchemaMock(); mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -88,7 +95,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -134,7 +146,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -182,7 +199,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -229,7 +251,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -267,7 +294,12 @@ describe('buildEventTypeSignal', () => { first.data[1].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -305,7 +337,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -329,7 +366,12 @@ describe('buildEventTypeSignal', () => { .mockReturnValueOnce(first) .mockReturnValueOnce(second); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); // Expect 2 exceptions, the first two calls returned the same exception list items expect(resp.entries.length).toEqual(2); @@ -340,7 +382,12 @@ describe('buildEventTypeSignal', () => { exceptionsResponse.data = []; exceptionsResponse.total = 0; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp.entries.length).toEqual(0); }); @@ -385,8 +432,18 @@ describe('buildEventTypeSignal', () => { ], }; - const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); - const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + const artifact1 = await buildArtifact( + translatedExceptionList, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + const artifact2 = await buildArtifact( + translatedExceptionListReversed, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); }); @@ -430,8 +487,18 @@ describe('buildEventTypeSignal', () => { entries: translatedItems.reverse(), }; - const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); - const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + const artifact1 = await buildArtifact( + translatedExceptionList, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + const artifact2 = await buildArtifact( + translatedExceptionListReversed, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index e41781dd605a0..731b083f3293c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -28,19 +28,20 @@ import { internalArtifactCompleteSchema, InternalArtifactCompleteSchema, } from '../../schemas'; -import { ArtifactConstants } from './common'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, os: string, - schemaVersion: string + schemaVersion: string, + name: string ): Promise { const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions)); const sha256 = createHash('sha256').update(exceptionsBuffer.toString()).digest('hex'); // Keep compression info empty in case its a duplicate. Lazily compress before committing if needed. return { - identifier: `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-${os}-${schemaVersion}`, + identifier: `${name}-${os}-${schemaVersion}`, compressionAlgorithm: 'none', encryptionAlgorithm: 'none', decodedSha256: sha256, @@ -76,7 +77,8 @@ export function isCompressed(artifact: InternalArtifactSchema) { export async function getFullEndpointExceptionList( eClient: ExceptionListClient, os: string, - schemaVersion: string + schemaVersion: string, + listId: typeof ENDPOINT_LIST_ID | typeof ENDPOINT_TRUSTED_APPS_LIST_ID ): Promise { const exceptions: WrappedTranslatedExceptionList = { entries: [] }; let page = 1; @@ -84,7 +86,7 @@ export async function getFullEndpointExceptionList( while (paging) { const response = await eClient.findExceptionListItem({ - listId: ENDPOINT_LIST_ID, + listId, namespaceType: 'agnostic', filter: `exception-list-agnostic.attributes._tags:\"os:${os}\"`, perPage: 100, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts index 3d70f7266277f..507b81b12e10b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -94,6 +94,36 @@ describe('manifest', () => { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + decoded_size: 432, + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_size: 147, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + decoded_size: 432, + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_size: 147, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + decoded_size: 432, + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_size: 147, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, }, manifest_version: '1.0.0', schema_version: 'v1', @@ -107,6 +137,9 @@ describe('manifest', () => { ids: [ 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ], }); }); @@ -119,6 +152,21 @@ describe('manifest', () => { 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, + { + id: + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, { id: 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', @@ -139,6 +187,9 @@ describe('manifest', () => { expect(keys).toEqual([ 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ]); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts index 61850bfb3bc7d..cdfbb551940e1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts @@ -16,13 +16,20 @@ import { ArtifactConstants } from './common'; import { Manifest } from './manifest'; export const getMockArtifacts = async (opts?: { compress: boolean }) => { - return Promise.all( - ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( + return Promise.all([ + // Exceptions items + ...ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( async (os) => { return getInternalArtifactMock(os, 'v1', opts); } - ) - ); + ), + // Trusted Apps items + ...ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS.map< + Promise + >(async (os) => { + return getInternalArtifactMock(os, 'v1', opts, ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME); + }), + ]); }; export const getMockArtifactsWithDiff = async (opts?: { compress: boolean }) => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts index b233ff1af30fc..5993b0b0e752e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts @@ -11,6 +11,8 @@ import { getHostPolicyResponseHandler } from './handlers'; export const BASE_POLICY_RESPONSE_ROUTE = `/api/endpoint/policy_response`; +export const INITIAL_POLICY_ID = '00000000-0000-0000-0000-000000000000'; + export function registerPolicyRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { router.get( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts index 7c8d006687a6b..f05d9ef5b821a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts @@ -5,6 +5,7 @@ */ import { GetPolicyResponseSchema } from '../../../../common/endpoint/schema/policy'; +import { getESQueryPolicyResponseByHostID } from './service'; describe('test policy handlers schema', () => { it('validate that get policy response query schema', async () => { @@ -17,3 +18,21 @@ describe('test policy handlers schema', () => { expect(() => GetPolicyResponseSchema.query.validate({})).toThrowError(); }); }); + +describe('test policy query', () => { + it('queries for the correct host', async () => { + const hostID = 'f757d3c0-e874-11ea-9ad9-015510b487f4'; + const query = getESQueryPolicyResponseByHostID(hostID, 'anyindex'); + expect(query.body.query.bool.filter.term).toEqual({ 'host.id': hostID }); + }); + + it('filters out initial policy by ID', async () => { + const query = getESQueryPolicyResponseByHostID( + 'f757d3c0-e874-11ea-9ad9-015510b487f4', + 'anyindex' + ); + expect(query.body.query.bool.must_not.term).toEqual({ + 'Endpoint.policy.applied.id': '00000000-0000-0000-0000-000000000000', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts index 703c46b05f766..1b3d232f9421c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts @@ -7,13 +7,23 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; +import { INITIAL_POLICY_ID } from './index'; export function getESQueryPolicyResponseByHostID(hostID: string, index: string) { return { body: { query: { - match: { - 'host.id': hostID, + bool: { + filter: { + term: { + 'host.id': hostID, + }, + }, + must_not: { + term: { + 'Endpoint.policy.applied.id': INITIAL_POLICY_ID, + }, + }, }, }, sort: [ 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 @@ -24,7 +54,7 @@ export const getTrustedAppsListRouteHandler = ( try { // Ensure list is created if it does not exist - await exceptionsListService?.createTrustedAppsList(); + await exceptionsListService.createTrustedAppsList(); const results = await exceptionsListService.findExceptionListItem({ listId: ENDPOINT_TRUSTED_APPS_LIST_ID, page, @@ -47,3 +77,32 @@ export const getTrustedAppsListRouteHandler = ( } }; }; + +export const getTrustedAppsCreateRouteHandler = ( + endpointAppContext: EndpointAppContext +): RequestHandler => { + const logger = endpointAppContext.logFactory.get('trusted_apps'); + + return async (constext, req, res) => { + const exceptionsListService = endpointAppContext.service.getExceptionsList(); + const newTrustedApp = req.body; + + try { + // Ensure list is created if it does not exist + await exceptionsListService.createTrustedAppsList(); + + const createdTrustedAppExceptionItem = await exceptionsListService.createExceptionListItem( + newTrustedAppItemToExceptionItem(newTrustedApp) + ); + + return res.ok({ + body: { + data: exceptionItemToTrustedAppItem(createdTrustedAppExceptionItem), + }, + }); + } catch (error) { + logger.error(error); + return res.internalError({ body: error }); + } + }; +}; 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 178aa06eee877..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 @@ -5,15 +5,37 @@ */ import { IRouter } from 'kibana/server'; -import { GetTrustedAppsRequestSchema } from '../../../../common/endpoint/schema/trusted_apps'; -import { TRUSTED_APPS_LIST_API } from '../../../../common/endpoint/constants'; -import { getTrustedAppsListRouteHandler } from './handlers'; +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, + 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( { @@ -23,4 +45,14 @@ export const registerTrustedAppsRoutes = ( }, getTrustedAppsListRouteHandler(endpointAppContext) ); + + // CREATE + router.post( + { + path: TRUSTED_APPS_CREATE_API, + validate: PostTrustedAppCreateRequestSchema, + options: { authRequired: true }, + }, + getTrustedAppsCreateRouteHandler(endpointAppContext) + ); }; 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 1d4a7919b89f5..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,15 +9,25 @@ 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_LIST_API } from '../../../../common/endpoint/constants'; -import { GetTrustedAppsListRequest } from '../../../../common/endpoint/types'; +import { + TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_DELETE_API, + TRUSTED_APPS_LIST_API, +} from '../../../../common/endpoint/constants'; +import { + GetTrustedAppsListRequest, + PostTrustedAppCreateRequest, +} from '../../../../common/endpoint/types'; import { xpackMocks } from '../../../../../../mocks'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; 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; @@ -105,4 +115,152 @@ describe('when invoking endpoint trusted apps route handlers', () => { expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); }); }); + + describe('when creating a trusted app', () => { + let routeHandler: RequestHandler; + const createNewTrustedAppBody = (): PostTrustedAppCreateRequest => ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: 'windows', + entries: [ + { + field: 'path', + type: 'match', + operator: 'included', + value: 'c:/programs files/Anti-Virus', + }, + ], + }); + const createPostRequest = () => { + return httpServerMock.createKibanaRequest({ + path: TRUSTED_APPS_LIST_API, + method: 'post', + body: createNewTrustedAppBody(), + }); + }; + + beforeEach(() => { + // Get the registered POST handler from the IRouter instance + [, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => + path.startsWith(TRUSTED_APPS_CREATE_API) + )!; + + // Mock the impelementation of `createExceptionListItem()` so that the return value + // merges in the provided input + exceptionsListClient.createExceptionListItem.mockImplementation(async (newExceptionItem) => { + return ({ + ...getExceptionListItemSchemaMock(), + ...newExceptionItem, + } as unknown) as ExceptionListItemSchema; + }); + }); + + it('should create trusted app list first', async () => { + const request = createPostRequest(); + await routeHandler(context, request, response); + expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); + expect(response.ok).toHaveBeenCalled(); + }); + + it('should map new trusted app item to an exception list item', async () => { + const request = createPostRequest(); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0]).toEqual({ + _tags: ['os:windows'], + comments: [], + description: 'this one is ok', + entries: [ + { + field: 'path', + operator: 'included', + type: 'match', + value: 'c:/programs files/Anti-Virus', + }, + ], + itemId: expect.stringMatching(/.*/), + listId: 'endpoint_trusted_apps', + meta: undefined, + name: 'Some Anti-Virus App', + namespaceType: 'agnostic', + tags: [], + type: 'simple', + }); + }); + + it('should return new trusted app item', async () => { + const request = createPostRequest(); + await routeHandler(context, request, response); + expect(response.ok.mock.calls[0][0]).toEqual({ + body: { + data: { + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'some user', + description: 'this one is ok', + entries: [ + { + field: 'path', + operator: 'included', + type: 'match', + value: 'c:/programs files/Anti-Virus', + }, + ], + id: '1', + name: 'Some Anti-Virus App', + os: 'windows', + }, + }, + }); + }); + + it('should log unexpected error if one occurs', async () => { + exceptionsListClient.createExceptionListItem.mockImplementation(() => { + throw new Error('expected error for create'); + }); + const request = createPostRequest(); + await routeHandler(context, request, response); + expect(response.internalError).toHaveBeenCalled(); + 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/endpoint/routes/trusted_apps/utils.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts index 2b417a4c6a8e1..794c1db4b49aa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import uuid from 'uuid'; import { ExceptionListItemSchema } from '../../../../../lists/common/shared_exports'; -import { TrustedApp } from '../../../../common/endpoint/types'; +import { NewTrustedApp, TrustedApp } from '../../../../common/endpoint/types'; +import { ExceptionListClient } from '../../../../../lists/server'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; + +type NewExecptionItem = Parameters[0]; /** * Map an ExcptionListItem to a TrustedApp item @@ -40,3 +45,28 @@ const osFromTagsList = (tags: string[]): TrustedApp['os'] | 'unknown' => { } return 'unknown'; }; + +export const newTrustedAppItemToExceptionItem = ({ + os, + entries, + name, + description = '', +}: NewTrustedApp): NewExecptionItem => { + return { + _tags: tagsListFromOs(os), + comments: [], + description, + entries, + itemId: uuid.v4(), + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + meta: undefined, + name, + namespaceType: 'agnostic', + tags: [], + type: 'simple', + }; +}; + +const tagsListFromOs = (os: NewTrustedApp['os']): NewExecptionItem['_tags'] => { + return [`os:${os}`]; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts index ae565f785c399..5d46fd0f2456b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { buildArtifact, maybeCompressArtifact, isCompressed } from '../../lib/artifacts'; +import { + buildArtifact, + maybeCompressArtifact, + isCompressed, + ArtifactConstants, +} from '../../lib/artifacts'; import { getTranslatedExceptionListMock } from './lists.mock'; import { InternalManifestSchema, @@ -25,9 +30,15 @@ const compressArtifact = async (artifact: InternalArtifactCompleteSchema) => { export const getInternalArtifactMock = async ( os: string, schemaVersion: string, - opts?: { compress: boolean } + opts?: { compress: boolean }, + artifactName: string = ArtifactConstants.GLOBAL_ALLOWLIST_NAME ): Promise => { - const artifact = await buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); + const artifact = await buildArtifact( + getTranslatedExceptionListMock(), + os, + schemaVersion, + artifactName + ); return opts?.compress ? compressArtifact(artifact) : artifact; }; @@ -36,7 +47,12 @@ export const getEmptyInternalArtifactMock = async ( schemaVersion: string, opts?: { compress: boolean } ): Promise => { - const artifact = await buildArtifact({ entries: [] }, os, schemaVersion); + const artifact = await buildArtifact( + { entries: [] }, + os, + schemaVersion, + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); return opts?.compress ? compressArtifact(artifact) : artifact; }; @@ -47,7 +63,12 @@ export const getInternalArtifactMockWithDiffs = async ( ): Promise => { const mock = getTranslatedExceptionListMock(); mock.entries.pop(); - const artifact = await buildArtifact(mock, os, schemaVersion); + const artifact = await buildArtifact( + mock, + os, + schemaVersion, + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); return opts?.compress ? compressArtifact(artifact) : artifact; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index bb6504de6e0a4..40b408166b17f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -24,11 +24,41 @@ describe('manifest_manager', () => { 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, + { + id: + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, { id: 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, + { + id: + 'endpoint-trustlist-macos-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-windows-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, ]); }); @@ -44,16 +74,53 @@ describe('manifest_manager', () => { 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, + { + id: + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, { id: 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, + { + id: + 'endpoint-trustlist-macos-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-windows-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, ]); - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); - const artifact = newManifest.getArtifact(newArtifactId)!; + const firstNewArtifactId = diffs.find((diff) => diff.type === 'add')!.id; + + // Compress all `add` artifacts + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } + + const artifact = newManifest.getArtifact(firstNewArtifactId)!; if (isCompleteArtifact(artifact)) { await manifestManager.pushArtifacts([artifact]); // caches the artifact @@ -61,7 +128,7 @@ describe('manifest_manager', () => { throw new Error('Artifact is missing a body.'); } - const entry = JSON.parse(inflateSync(cache.get(newArtifactId)! as Buffer).toString()); + const entry = JSON.parse(inflateSync(cache.get(firstNewArtifactId)! as Buffer).toString()); expect(entry).toEqual({ entries: [ { @@ -107,8 +174,12 @@ describe('manifest_manager', () => { const oldManifest = await manifestManager.getLastComputedManifest(); const newManifest = await manifestManager.buildNewManifest(oldManifest!); const diffs = newManifest.diff(oldManifest!); - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); + + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } newManifest.bumpSemanticVersion(); @@ -145,6 +216,36 @@ describe('manifest_manager', () => { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, }, }); }); @@ -155,8 +256,12 @@ describe('manifest_manager', () => { const oldManifest = await manifestManager.getLastComputedManifest(); const newManifest = await manifestManager.buildNewManifest(oldManifest!); const diffs = newManifest.diff(oldManifest!); - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); + + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } newManifest.bumpSemanticVersion(); @@ -174,11 +279,17 @@ describe('manifest_manager', () => { const oldManifest = await manifestManager.getLastComputedManifest(); const newManifest = await manifestManager.buildNewManifest(oldManifest!); const diffs = newManifest.diff(oldManifest!); - const oldArtifactId = diffs[0].id; - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); + const firstOldArtifactId = diffs.find((diff) => diff.type === 'delete')!.id; + const FirstNewArtifactId = diffs.find((diff) => diff.type === 'add')!.id; + + // Compress all new artifacts + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } - const artifact = newManifest.getArtifact(newArtifactId)!; + const artifact = newManifest.getArtifact(FirstNewArtifactId)!; if (isCompleteArtifact(artifact)) { await manifestManager.pushArtifacts([artifact]); } else { @@ -186,7 +297,7 @@ describe('manifest_manager', () => { } await manifestManager.commit(newManifest); - await manifestManager.deleteArtifacts([oldArtifactId]); + await manifestManager.deleteArtifacts([firstOldArtifactId]); // created new artifact expect(savedObjectsClient.create.mock.calls[0][0]).toEqual( @@ -201,7 +312,7 @@ describe('manifest_manager', () => { // deleted old artifact expect(savedObjectsClient.delete).toHaveBeenCalledWith( ArtifactConstants.SAVED_OBJECT_TYPE, - oldArtifactId + firstOldArtifactId ); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 70557886e57c5..f9836c7ecdc30 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -13,11 +13,11 @@ import { manifestDispatchSchema } from '../../../../../common/endpoint/schema/ma import { ArtifactConstants, - Manifest, buildArtifact, + getArtifactId, getFullEndpointExceptionList, + Manifest, ManifestDiff, - getArtifactId, } from '../../../lib/artifacts'; import { InternalArtifactCompleteSchema, @@ -25,6 +25,8 @@ import { } from '../../../schemas/artifacts'; import { ArtifactClient } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; +import { ENDPOINT_LIST_ID } from '../../../../../../lists/common'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common/constants'; export interface ManifestManagerContext { savedObjectsClient: SavedObjectsClientContract; @@ -87,9 +89,43 @@ export class ManifestManager { const exceptionList = await getFullEndpointExceptionList( this.exceptionListClient, os, - artifactSchemaVersion ?? 'v1' + artifactSchemaVersion ?? 'v1', + ENDPOINT_LIST_ID + ); + const artifact = await buildArtifact( + exceptionList, + os, + artifactSchemaVersion ?? 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + artifacts.push(artifact); + } + return artifacts; + } + + /** + * Builds an array of artifacts (one per supported OS) based on the current state of the + * Trusted Apps list (which uses the `exception-list-agnostic` SO type) + * @param artifactSchemaVersion + */ + protected async buildTrustedAppsArtifacts( + artifactSchemaVersion?: string + ): Promise { + const artifacts: InternalArtifactCompleteSchema[] = []; + + for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) { + const trustedApps = await getFullEndpointExceptionList( + this.exceptionListClient, + os, + artifactSchemaVersion ?? 'v1', + ENDPOINT_TRUSTED_APPS_LIST_ID + ); + const artifact = await buildArtifact( + trustedApps, + os, + 'v1', + ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME ); - const artifact = await buildArtifact(exceptionList, os, artifactSchemaVersion ?? 'v1'); artifacts.push(artifact); } return artifacts; @@ -205,7 +241,9 @@ export class ManifestManager { */ public async buildNewManifest(baselineManifest?: Manifest): Promise { // Build new exception list artifacts - const artifacts = await this.buildExceptionListArtifacts(); + const artifacts = ( + await Promise.all([this.buildExceptionListArtifacts(), this.buildTrustedAppsArtifacts()]) + ).flat(); // Build new manifest const manifest = Manifest.fromArtifacts( diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts index bdc69f85d3542..60c2ce8ceca64 100644 --- a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts @@ -424,6 +424,7 @@ export const ecsSchema = gql` type SignalField { rule: RuleField original_time: ToStringArray + status: ToStringArray } type RuleEcsField { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index fa55af351651e..7638ebd03f6b1 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -1022,6 +1022,8 @@ export interface SignalField { rule?: Maybe; original_time?: Maybe; + + status?: Maybe; } export interface RuleField { @@ -4930,6 +4932,8 @@ export namespace SignalFieldResolvers { rule?: RuleResolver, TypeParent, TContext>; original_time?: OriginalTimeResolver, TypeParent, TContext>; + + status?: StatusResolver, TypeParent, TContext>; } export type RuleResolver< @@ -4942,6 +4946,11 @@ export namespace SignalFieldResolvers { Parent = SignalField, TContext = SiemContext > = Resolver; + export type StatusResolver< + R = Maybe, + Parent = SignalField, + TContext = SiemContext + > = Resolver; } export namespace RuleFieldResolvers { 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/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index ac5132d93a069..be6e57aee6d0c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -28,11 +28,12 @@ export const setSignalsStatusRoute = (router: IRouter) => { }, }, async (context, request, response) => { - const { signal_ids: signalIds, query, status } = request.body; + const { conflicts, signal_ids: signalIds, query, status } = request.body; const clusterClient = context.core.elasticsearch.legacy.client; const siemClient = context.securitySolution?.getAppClient(); const siemResponse = buildSiemResponse(response); const validationErrors = setSignalStatusValidateTypeDependents(request.body); + if (validationErrors.length) { return siemResponse.error({ statusCode: 400, body: validationErrors }); } @@ -55,6 +56,7 @@ export const setSignalsStatusRoute = (router: IRouter) => { try { const result = await clusterClient.callAsCurrentUser('updateByQuery', { index: siemClient.getSignalsIndex(), + conflicts: conflicts ?? 'abort', refresh: 'wait_for', body: { script: { 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/ecs_fields/index.ts b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts index 19b16bd4bc6d2..d1c8290b3462d 100644 --- a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts +++ b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts @@ -324,6 +324,7 @@ export const signalFieldsMap: Readonly> = { 'signal.rule.note': 'signal.rule.note', 'signal.rule.threshold': 'signal.rule.threshold', 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', + 'signal.status': 'signal.status', }; export const ruleFieldsMap: Readonly> = { 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__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts index 245146dda183f..90d5b538a5200 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -5,7 +5,7 @@ */ import { omit } from 'lodash/fp'; -import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; export const mockDuplicateIdErrors = []; @@ -332,8 +332,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = { value: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509`, enabled: true, }, ], @@ -496,8 +495,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = { value: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d`, enabled: true, }, ], @@ -675,8 +673,7 @@ export const mockCheckTimelinesStatusBeforeInstallResult = { value: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde`, enabled: true, }, ], @@ -848,8 +845,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = { value: '30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-30d47c8a1b179ae435058e5b23b96118125a451fe58efd77be288f00456ff77d`, enabled: true, }, ], @@ -1031,8 +1027,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = { value: '590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-590eb946a7fdbacaa587ed0f6b1a16f5ad3d659ec47ef35ad0826c47af133bde`, enabled: true, }, ], @@ -1152,8 +1147,7 @@ export const mockCheckTimelinesStatusAfterInstallResult = { value: '3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', operator: ':', }, - id: - 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509', + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-signal-id-3c322ed995865f642c1a269d54cbd177bd4b0e6efcf15a589f4f8582efbe7509`, enabled: true, }, ], 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 new file mode 100644 index 0000000000000..3550824028478 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -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 { 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 { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; + +const HOSTS_FIELDS = ['_id', 'lastSeen', 'host.id', 'host.name', 'host.os.name', 'host.os.version']; + +export const formatHostEdgesData = (bucket: HostAggEsItem): HostsEdges => + HOSTS_FIELDS.reduce( + (flattenedFields, fieldName) => { + const hostId = get('key', bucket); + flattenedFields.node._id = hostId || null; + flattenedFields.cursor.value = hostId || ''; + const fieldValue = getHostFieldValue(fieldName, bucket); + if (fieldValue != null) { + return set( + `node.${fieldName}`, + Array.isArray(fieldValue) ? fieldValue : [fieldValue], + flattenedFields + ); + } + return flattenedFields; + }, + { + node: {}, + cursor: { + value: '', + tiebreaker: null, + }, + } as HostsEdges + ); + +const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | string[] | null => { + const aggField = hostFieldsMap[fieldName] + ? hostFieldsMap[fieldName].replace(/\./g, '_') + : fieldName.replace(/\./g, '_'); + if ( + [ + 'host.ip', + 'host.mac', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + ].includes(fieldName) && + has(aggField, bucket) + ) { + const data: HostBuckets = get(aggField, bucket); + return data.buckets.map((obj) => obj.key); + } else if (has(`${aggField}.buckets`, bucket)) { + return getFirstItem(get(`${aggField}`, bucket)); + } else if (has(aggField, bucket)) { + const valueObj: HostValue = get(aggField, bucket); + return valueObj.value_as_string; + } else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) { + switch (fieldName) { + case 'host.name': + return get('key', bucket) || null; + case 'host.os.name': + return get('os.hits.hits[0]._source.host.os.name', bucket) || null; + case 'host.os.version': + return get('os.hits.hits[0]._source.host.os.version', bucket) || null; + } + } + return null; +}; + +const getFirstItem = (data: HostBuckets): string | null => { + const firstItem = head(data.buckets); + if (firstItem == null) { + return null; + } + return firstItem.key; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts new file mode 100644 index 0000000000000..d4c2214b98645 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/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 { + HostAggEsItem, + HostsStrategyResponse, + HostsQueries, + HostsRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/hosts'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; +import { buildHostsQuery } from './query.all_hosts.dsl'; +import { formatHostEdgesData } from './helpers'; + +export const allHosts: SecuritySolutionFactory = { + buildDsl: (options: HostsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildHostsQuery(options); + }, + parse: async ( + options: HostsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.host_count.value', response.rawResponse); + const buckets: HostAggEsItem[] = getOr( + [], + 'aggregations.host_data.buckets', + response.rawResponse + ); + const hostsEdges = buckets.map((bucket) => formatHostEdgesData(bucket)); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = hostsEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildHostsQuery(options))], + response: [inspectStringifyObject(response)], + }; + 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/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 new file mode 100644 index 0000000000000..93390c314a637 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.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 { isEmpty, isString } from 'lodash/fp'; +import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { + Direction, + HostsRequestOptions, + SortField, + HostsFields, +} from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; +import { assertUnreachable } from '../../../../../../common/utility_types'; + +export const buildHostsQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + pagination: { querySize }, + sort, + timerange: { from, to }, +}: HostsRequestOptions): ISearchRequestParams => { + const filter = [ + ...createQueryFilterClauses(isString(filterQuery) ? JSON.parse(filterQuery) : filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const agg = { host_count: { cardinality: { field: 'host.name' } } }; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggregations: { + ...agg, + host_data: { + terms: { size: querySize, field: 'host.name', order: getQueryOrder(sort) }, + aggs: { + lastSeen: { max: { field: '@timestamp' } }, + os: { + top_hits: { + size: 1, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + _source: { + includes: ['host.os.*'], + }, + }, + }, + }, + }, + }, + query: { bool: { filter } }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; + +type QueryOrder = { lastSeen: Direction } | { _key: Direction }; + +const getQueryOrder = (sort: SortField): QueryOrder => { + switch (sort.field) { + case HostsFields.lastSeen: + return { lastSeen: sort.direction }; + case HostsFields.hostName: + return { _key: sort.direction }; + default: + return assertUnreachable(sort.field as never); + } +}; 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/dsl/query.detail_host.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.detail_host.dsl.ts deleted file mode 100644 index 5c5dec92a5100..0000000000000 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.detail_host.dsl.ts +++ /dev/null @@ -1,49 +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 { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; -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'; - -export const buildHostOverviewQuery = ({ - fields, - hostName, - defaultIndex, - timerange: { from, to }, -}: HostOverviewRequestOptions): ISearchRequestParams => { - const esFields = reduceFields(fields, { ...hostFieldsMap, ...cloudFieldsMap }); - - const filter = [ - { term: { 'host.name': hostName } }, - { - range: { - '@timestamp': { - format: 'strict_date_optional_time', - gte: from, - lte: to, - }, - }, - }, - ]; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - aggregations: { - ...buildFieldsTermAggregation(esFields.filter((field) => !['@timestamp'].includes(field))), - }, - 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/dsl/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts deleted file mode 100644 index a9101f54ada55..0000000000000 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts +++ /dev/null @@ -1,87 +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 { isEmpty, isString } from 'lodash/fp'; -import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; -import { - Direction, - HostsRequestOptions, - SortField, -} from '../../../../../../common/search_strategy/security_solution'; -import { createQueryFilterClauses } from '../../../../../utils/build_query'; - -export const buildHostsQuery = ({ - defaultIndex, - docValueFields, - filterQuery, - pagination: { querySize }, - sort, - timerange: { from, to }, -}: HostsRequestOptions): ISearchRequestParams => { - const filter = [ - ...createQueryFilterClauses(isString(filterQuery) ? JSON.parse(filterQuery) : filterQuery), - { - range: { - '@timestamp': { - gte: from, - lte: to, - format: 'strict_date_optional_time', - }, - }, - }, - ]; - - const agg = { host_count: { cardinality: { field: 'host.name' } } }; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), - aggregations: { - ...agg, - host_data: { - terms: { size: querySize, field: 'host.name', order: getQueryOrder(sort) }, - aggs: { - lastSeen: { max: { field: '@timestamp' } }, - os: { - top_hits: { - size: 1, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - _source: { - includes: ['host.os.*'], - }, - }, - }, - }, - }, - }, - query: { bool: { filter } }, - size: 0, - track_total_hits: false, - }, - }; - - return dslQuery; -}; - -type QueryOrder = { lastSeen: Direction } | { _key: Direction }; - -const getQueryOrder = (sort: SortField): QueryOrder => { - switch (sort.field) { - case 'lastSeen': - return { lastSeen: sort.direction }; - case 'hostName': - return { _key: sort.direction }; - } -}; 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 443e524d71ca3..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,89 +4,22 @@ * 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 { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; -import { FactoryQueryTypes } from '../../../../../common/search_strategy/security_solution'; import { - HostsStrategyResponse, - HostOverviewStrategyResponse, + FactoryQueryTypes, HostsQueries, - HostsRequestOptions, - HostOverviewRequestOptions, -} from '../../../../../common/search_strategy/security_solution/hosts'; +} from '../../../../../common/search_strategy/security_solution'; -// TO DO need to move all this types in common -import { HostAggEsData, HostAggEsItem } from '../../../../lib/hosts/types'; - -import { inspectStringifyObject } from '../../../../utils/build_query'; import { SecuritySolutionFactory } from '../types'; -import { buildHostOverviewQuery } from './dsl/query.detail_host.dsl'; -import { buildHostsQuery } from './dsl/query.hosts.dsl'; -import { formatHostEdgesData, formatHostItem } from './helpers'; - -export const allHosts: SecuritySolutionFactory = { - buildDsl: (options: HostsRequestOptions) => { - if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); - } - return buildHostsQuery(options); - }, - parse: async ( - options: HostsRequestOptions, - response: IEsSearchResponse - ): Promise => { - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.host_count.value', response.rawResponse); - const buckets: HostAggEsItem[] = getOr( - [], - 'aggregations.host_data.buckets', - response.rawResponse - ); - const hostsEdges = buckets.map((bucket) => formatHostEdgesData(bucket)); - const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = hostsEdges.splice(cursorStart, querySize - cursorStart); - const inspect = { - dsl: [inspectStringifyObject(buildHostsQuery(options))], - response: [inspectStringifyObject(response)], - }; - const showMorePagesIndicator = totalCount > fakeTotalCount; - - return { - ...response, - inspect, - edges, - totalCount, - pageInfo: { - activePage: activePage ? activePage : 0, - fakeTotalCount, - showMorePagesIndicator, - }, - }; - }, -}; - -export const overviewHost: SecuritySolutionFactory = { - buildDsl: (options: HostOverviewRequestOptions) => { - return buildHostOverviewQuery(options); - }, - parse: async ( - options: HostOverviewRequestOptions, - response: IEsSearchResponse - ): Promise => { - const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; - const inspect = { - dsl: [inspectStringifyObject(buildHostOverviewQuery(options))], - response: [inspectStringifyObject(response)], - }; - const formattedHostItem = formatHostItem(aggregations); - return { ...response, inspect, _id: options.hostName, ...formattedHostItem }; - }, -}; +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/last_first_seen/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts new file mode 100644 index 0000000000000..56895583c2ae9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts @@ -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 { get } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + HostAggEsData, + HostAggEsItem, + HostFirstLastSeenStrategyResponse, + HostsQueries, + HostFirstLastSeenRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/hosts'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; +import { buildFirstLastSeenHostQuery } from './query.last_first_seen_host.dsl'; + +export const firstLastSeenHost: SecuritySolutionFactory = { + buildDsl: (options: HostFirstLastSeenRequestOptions) => buildFirstLastSeenHostQuery(options), + parse: async ( + options: HostFirstLastSeenRequestOptions, + response: IEsSearchResponse + ): Promise => { + const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; + const inspect = { + dsl: [inspectStringifyObject(buildFirstLastSeenHostQuery(options))], + response: [inspectStringifyObject(response)], + }; + + return { + ...response, + inspect, + firstSeen: get('firstSeen.value_as_string', aggregations), + lastSeen: get('lastSeen.value_as_string', aggregations), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.last_first_seen_host.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.ts similarity index 80% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.last_first_seen_host.dsl.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.ts index b57bbd2960e4f..2c65f62b258a9 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.last_first_seen_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.ts @@ -6,13 +6,13 @@ import { isEmpty } from 'lodash/fp'; import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; -import { HostLastFirstSeenRequestOptions } from '../../../../../../common/search_strategy/security_solution'; +import { HostFirstLastSeenRequestOptions } from '../../../../../../common/search_strategy/security_solution/hosts'; -export const buildLastFirstSeenHostQuery = ({ +export const buildFirstLastSeenHostQuery = ({ hostName, defaultIndex, docValueFields, -}: HostLastFirstSeenRequestOptions): ISearchRequestParams => { +}: HostFirstLastSeenRequestOptions): ISearchRequestParams => { const filter = [{ term: { 'host.name': hostName } }]; const dslQuery = { 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 new file mode 100644 index 0000000000000..ed705e7f6ad56 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { set } from '@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 { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; + +export const HOST_FIELDS = [ + '_id', + 'host.architecture', + 'host.id', + 'host.ip', + 'host.id', + 'host.mac', + 'host.name', + 'host.os.family', + 'host.os.name', + 'host.os.platform', + 'host.os.version', + 'host.type', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + 'endpoint.endpointPolicy', + 'endpoint.policyStatus', + 'endpoint.sensorVersion', +]; + +export const formatHostItem = (bucket: HostAggEsItem): HostItem => + HOST_FIELDS.reduce((flattenedFields, fieldName) => { + const fieldValue = getHostFieldValue(fieldName, bucket); + if (fieldValue != null) { + return set(fieldName, fieldValue, flattenedFields); + } + return flattenedFields; + }, {}); + +const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | string[] | null => { + const aggField = hostFieldsMap[fieldName] + ? hostFieldsMap[fieldName].replace(/\./g, '_') + : fieldName.replace(/\./g, '_'); + if ( + [ + 'host.ip', + 'host.mac', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + ].includes(fieldName) && + has(aggField, bucket) + ) { + const data: HostBuckets = get(aggField, bucket); + return data.buckets.map((obj) => obj.key); + } else if (has(`${aggField}.buckets`, bucket)) { + return getFirstItem(get(`${aggField}`, bucket)); + } else if (has(aggField, bucket)) { + const valueObj: HostValue = get(aggField, bucket); + return valueObj.value_as_string; + } else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) { + switch (fieldName) { + case 'host.name': + return get('key', bucket) || null; + case 'host.os.name': + return get('os.hits.hits[0]._source.host.os.name', bucket) || null; + case 'host.os.version': + return get('os.hits.hits[0]._source.host.os.version', bucket) || null; + } + } + return null; +}; + +const getFirstItem = (data: HostBuckets): string | null => { + const firstItem = head(data.buckets); + if (firstItem == null) { + return null; + } + return firstItem.key; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts new file mode 100644 index 0000000000000..8bdda9ef895b2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.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 { get } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + HostAggEsData, + HostAggEsItem, + HostOverviewStrategyResponse, + HostsQueries, + HostOverviewRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/hosts'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; +import { buildHostOverviewQuery } from './query.host_overview.dsl'; +import { formatHostItem } from './helpers'; + +export const overviewHost: SecuritySolutionFactory = { + buildDsl: (options: HostOverviewRequestOptions) => { + return buildHostOverviewQuery(options); + }, + parse: async ( + options: HostOverviewRequestOptions, + response: IEsSearchResponse + ): Promise => { + const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; + const inspect = { + dsl: [inspectStringifyObject(buildHostOverviewQuery(options))], + response: [inspectStringifyObject(response)], + }; + const formattedHostItem = formatHostItem(aggregations); + + return { ...response, inspect, hostOverview: formattedHostItem }; + }, +}; 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 new file mode 100644 index 0000000000000..85cc87414c38e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts @@ -0,0 +1,49 @@ +/* + * 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 { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { cloudFieldsMap, hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; +import { HostOverviewRequestOptions } from '../../../../../../common/search_strategy/security_solution'; +import { buildFieldsTermAggregation } from '../../../../../lib/hosts/helpers'; +import { reduceFields } from '../../../../../utils/build_query/reduce_fields'; +import { HOST_FIELDS } from './helpers'; + +export const buildHostOverviewQuery = ({ + hostName, + defaultIndex, + timerange: { from, to }, +}: HostOverviewRequestOptions): ISearchRequestParams => { + const esFields = reduceFields(HOST_FIELDS, { ...hostFieldsMap, ...cloudFieldsMap }); + + const filter = [ + { term: { 'host.name': hostName } }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: from, + lte: to, + }, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggregations: { + ...buildFieldsTermAggregation(esFields.filter((field) => !['@timestamp'].includes(field))), + }, + 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/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/dns/query.dns_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts index 1ff4880e29050..85b9051189bfe 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts @@ -9,10 +9,10 @@ import { isEmpty } from 'lodash/fp'; import { assertUnreachable } from '../../../../../../common/utility_types'; import { Direction, + SortField, NetworkDnsRequestOptions, NetworkDnsFields, - NetworkDnsSortField, -} from '../../../../../../common/search_strategy/security_solution/network'; +} from '../../../../../../common/search_strategy'; import { createQueryFilterClauses } from '../../../../../utils/build_query'; type QueryOrder = @@ -22,20 +22,20 @@ type QueryOrder = | { dns_bytes_in: Direction } | { dns_bytes_out: Direction }; -const getQueryOrder = (networkDnsSortField: NetworkDnsSortField): QueryOrder => { - switch (networkDnsSortField.field) { +const getQueryOrder = (sort: SortField): QueryOrder => { + switch (sort.field) { case NetworkDnsFields.queryCount: - return { _count: networkDnsSortField.direction }; + return { _count: sort.direction }; case NetworkDnsFields.dnsName: - return { _key: networkDnsSortField.direction }; + return { _key: sort.direction }; case NetworkDnsFields.uniqueDomains: - return { unique_domains: networkDnsSortField.direction }; + return { unique_domains: sort.direction }; case NetworkDnsFields.dnsBytesIn: - return { dns_bytes_in: networkDnsSortField.direction }; + return { dns_bytes_in: sort.direction }; case NetworkDnsFields.dnsBytesOut: - return { dns_bytes_out: networkDnsSortField.direction }; + return { dns_bytes_out: sort.direction }; } - assertUnreachable(networkDnsSortField.field); + assertUnreachable(sort.field); }; const getCountAgg = () => ({ @@ -66,7 +66,7 @@ export const buildDnsQuery = ({ docValueFields, filterQuery, isPtrIncluded, - networkDnsSortField, + sort, pagination: { querySize }, stackByField = 'dns.question.registered_domain', timerange: { from, to }, @@ -97,7 +97,7 @@ export const buildDnsQuery = ({ field: stackByField, size: querySize, order: { - ...getQueryOrder(networkDnsSortField), + ...getQueryOrder(sort), }, }, aggs: { 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 index c6393e6a289d7..a7fba087b87ed 100644 --- 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 @@ -5,12 +5,7 @@ */ import { assertUnreachable } from '../../../../../common/utility_types'; -import { - Direction, - NetworkTopTablesSortField, - FlowTargetSourceDest, - NetworkTopTablesFields, -} from '../../../../../common/search_strategy/security_solution/network'; +import { FlowTargetSourceDest } from '../../../../../common/search_strategy/security_solution/network'; export const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => { switch (flowTarget) { @@ -21,28 +16,3 @@ export const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSo } assertUnreachable(flowTarget); }; - -type QueryOrder = - | { bytes_in: Direction } - | { bytes_out: Direction } - | { flows: Direction } - | { destination_ips: Direction } - | { source_ips: Direction }; - -export const getQueryOrder = ( - networkTopCountriesSortField: NetworkTopTablesSortField -): 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/security_solution/factory/network/http/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/helpers.ts index fee1c1c95c244..b8a28441337c7 100644 --- 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 @@ -13,7 +13,7 @@ import { } from '../../../../../../common/search_strategy/security_solution/network'; export const getHttpEdges = (response: IEsSearchResponse): NetworkHttpEdges[] => - formatHttpEdges(getOr([], `aggregations.url.buckets`, response)); + formatHttpEdges(getOr([], `aggregations.url.buckets`, response.rawResponse)); const formatHttpEdges = (buckets: NetworkHttpBuckets[]): NetworkHttpEdges[] => buckets.map((bucket: NetworkHttpBuckets) => ({ 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 index b6c26cd533de2..c0205ccce63cc 100644 --- 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 @@ -40,7 +40,6 @@ export const networkHttp: SecuritySolutionFactory = { const edges = networkHttpEdges.splice(cursorStart, querySize - cursorStart); const inspect = { dsl: [inspectStringifyObject(buildHttpQuery(options))], - response: [inspectStringifyObject(response)], }; const showMorePagesIndicator = totalCount > fakeTotalCount; 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 index 9c655ca0f4eaa..feffe7f70afd9 100644 --- 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 @@ -6,8 +6,7 @@ import { createQueryFilterClauses } from '../../../../../utils/build_query'; -import { NetworkHttpRequestOptions } from '../../../../../../common/search_strategy/security_solution/network'; -import { NetworkHttpSortField } from '../../../../../graphql/types'; +import { NetworkHttpRequestOptions, SortField } from '../../../../../../common/search_strategy'; const getCountAgg = () => ({ http_count: { @@ -20,7 +19,7 @@ const getCountAgg = () => ({ export const buildHttpQuery = ({ defaultIndex, filterQuery, - networkHttpSort, + sort, pagination: { querySize }, timerange: { from, to }, ip, @@ -42,7 +41,7 @@ export const buildHttpQuery = ({ body: { aggregations: { ...getCountAgg(), - ...getHttpAggs(networkHttpSort, querySize), + ...getHttpAggs(sort, querySize), }, query: { bool: ip @@ -73,13 +72,13 @@ export const buildHttpQuery = ({ return dslQuery; }; -const getHttpAggs = (networkHttpSortField: NetworkHttpSortField, querySize: number) => ({ +const getHttpAggs = (sortField: SortField, querySize: number) => ({ url: { terms: { field: `url.path`, size: querySize, order: { - _count: networkHttpSortField.direction, + _count: sortField.direction, }, }, aggs: { 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 51e12db625042..f2b4f85174797 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,8 +4,10 @@ * 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 { networkDns } from './dns'; 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 407cff126e65d..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 @@ -7,10 +7,14 @@ import { assertUnreachable } from '../../../../../../common/utility_types'; import { createQueryFilterClauses } from '../../../../../utils/build_query'; -import { NetworkTlsRequestOptions } from '../../../../../../common/search_strategy/security_solution/network'; -import { TlsSortField, Direction, TlsFields } from '../../../../../graphql/types'; +import { + NetworkTlsRequestOptions, + SortField, + Direction, + TlsFields, +} from '../../../../../../common/search_strategy'; -const getAggs = (querySize: number, sort: TlsSortField) => ({ +const getAggs = (querySize: number, sort: SortField) => ({ count: { cardinality: { field: 'tls.server.hash.sha1', @@ -94,7 +98,7 @@ interface QueryOrder { _key: Direction; } -const getQueryOrder = (sort: TlsSortField): QueryOrder => { +const getQueryOrder = (sort: SortField): QueryOrder => { switch (sort.field) { case TlsFields._id: return { _key: sort.direction }; 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 index 58cc2d6f774fb..a8972c3d89f36 100644 --- 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 @@ -20,7 +20,7 @@ export const getTopCountriesEdges = ( options: NetworkTopCountriesRequestOptions ): NetworkTopCountriesEdges[] => formatTopCountriesEdges( - getOr([], `aggregations.${options.flowTarget}.buckets`, response), + getOr([], `aggregations.${options.flowTarget}.buckets`, response.rawResponse), options.flowTarget ); 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 index 24ecb32df1803..4f4b347e4db02 100644 --- 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 @@ -9,10 +9,10 @@ import { assertUnreachable } from '../../../../../../common/utility_types'; import { Direction, FlowTargetSourceDest, - NetworkTopTablesSortField, NetworkTopTablesFields, NetworkTopCountriesRequestOptions, -} from '../../../../../../common/search_strategy/security_solution/network'; + SortField, +} from '../../../../../../common/search_strategy'; const getCountAgg = (flowTarget: FlowTargetSourceDest) => ({ top_countries_count: { @@ -26,7 +26,7 @@ export const buildTopCountriesQuery = ({ defaultIndex, filterQuery, flowTarget, - networkTopCountriesSort, + sort, pagination: { querySize }, timerange: { from, to }, ip, @@ -47,7 +47,7 @@ export const buildTopCountriesQuery = ({ body: { aggregations: { ...getCountAgg(flowTarget), - ...getFlowTargetAggs(networkTopCountriesSort, flowTarget, querySize), + ...getFlowTargetAggs(sort, flowTarget, querySize), }, query: { bool: ip @@ -74,7 +74,7 @@ export const buildTopCountriesQuery = ({ }; const getFlowTargetAggs = ( - networkTopCountriesSortField: NetworkTopTablesSortField, + sort: SortField, flowTarget: FlowTargetSourceDest, querySize: number ) => ({ @@ -83,7 +83,7 @@ const getFlowTargetAggs = ( field: `${flowTarget}.geo.country_iso_code`, size: querySize, order: { - ...getQueryOrder(networkTopCountriesSortField), + ...getQueryOrder(sort), }, }, aggs: { @@ -133,7 +133,9 @@ type QueryOrder = | { destination_ips: Direction } | { source_ips: Direction }; -const getQueryOrder = (networkTopCountriesSortField: NetworkTopTablesSortField): QueryOrder => { +const getQueryOrder = ( + networkTopCountriesSortField: SortField +): QueryOrder => { switch (networkTopCountriesSortField.field) { case NetworkTopTablesFields.bytes_in: return { bytes_in: networkTopCountriesSortField.direction }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts index 098fd43cd03fa..720661e12bd96 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts @@ -6,15 +6,19 @@ import { getOr } from 'lodash/fp'; +import { assertUnreachable } from '../../../../../../common/utility_types'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { + Direction, GeoItem, + SortField, NetworkTopNFlowBuckets, NetworkTopNFlowEdges, NetworkTopNFlowRequestOptions, + NetworkTopTablesFields, FlowTargetSourceDest, AutonomousSystemItem, -} from '../../../../../../common/search_strategy/security_solution/network'; +} from '../../../../../../common/search_strategy'; import { getOppositeField } from '../helpers'; export const getTopNFlowEdges = ( @@ -95,3 +99,28 @@ const getAsItem = (result: NetworkTopNFlowBuckets): AutonomousSystemItem | null ), } : null; + +type QueryOrder = + | { bytes_in: Direction } + | { bytes_out: Direction } + | { flows: Direction } + | { destination_ips: Direction } + | { source_ips: Direction }; + +export const getQueryOrder = ( + networkTopNFlowSortField: SortField +): QueryOrder => { + switch (networkTopNFlowSortField.field) { + case NetworkTopTablesFields.bytes_in: + return { bytes_in: networkTopNFlowSortField.direction }; + case NetworkTopTablesFields.bytes_out: + return { bytes_out: networkTopNFlowSortField.direction }; + case NetworkTopTablesFields.flows: + return { flows: networkTopNFlowSortField.direction }; + case NetworkTopTablesFields.destination_ips: + return { destination_ips: networkTopNFlowSortField.direction }; + case NetworkTopTablesFields.source_ips: + return { source_ips: networkTopNFlowSortField.direction }; + } + assertUnreachable(networkTopNFlowSortField.field); +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.ts index b9014f76b8def..198368d981800 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.ts @@ -40,7 +40,6 @@ export const networkTopNFlow: SecuritySolutionFactory = const edges = networkTopNFlowEdges.splice(cursorStart, querySize - cursorStart); const inspect = { dsl: [inspectStringifyObject(buildTopNFlowQuery(options))], - response: [inspectStringifyObject(response)], }; const showMorePagesIndicator = totalCount > fakeTotalCount; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.ts index 49c51117d2b05..374dfa4d485fa 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.ts @@ -5,12 +5,14 @@ */ import { + SortField, FlowTargetSourceDest, - NetworkTopTablesSortField, + NetworkTopTablesFields, NetworkTopNFlowRequestOptions, -} from '../../../../../../common/search_strategy/security_solution/network'; +} from '../../../../../../common/search_strategy'; import { createQueryFilterClauses } from '../../../../../utils/build_query'; -import { getOppositeField, getQueryOrder } from '../helpers'; +import { getOppositeField } from '../helpers'; +import { getQueryOrder } from './helpers'; const getCountAgg = (flowTarget: FlowTargetSourceDest) => ({ top_n_flow_count: { @@ -24,7 +26,7 @@ export const buildTopNFlowQuery = ({ defaultIndex, filterQuery, flowTarget, - networkTopNFlowSort, + sort, pagination: { querySize }, timerange: { from, to }, ip, @@ -45,7 +47,7 @@ export const buildTopNFlowQuery = ({ body: { aggregations: { ...getCountAgg(flowTarget), - ...getFlowTargetAggs(networkTopNFlowSort, flowTarget, querySize), + ...getFlowTargetAggs(sort, flowTarget, querySize), }, query: { bool: ip @@ -72,7 +74,7 @@ export const buildTopNFlowQuery = ({ }; const getFlowTargetAggs = ( - networkTopNFlowSortField: NetworkTopTablesSortField, + sort: SortField, flowTarget: FlowTargetSourceDest, querySize: number ) => ({ @@ -81,7 +83,7 @@ const getFlowTargetAggs = ( field: `${flowTarget}.ip`, size: querySize, order: { - ...getQueryOrder(networkTopNFlowSortField), + ...getQueryOrder(sort), }, }, aggs: { 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/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index 0abc47686a6b4..6021ab2a42c90 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -20,13 +20,6 @@ import { REPOSITORY_NAME } from './helpers/constant'; const { setup } = pageHelpers.home; -jest.mock('ui/new_platform'); - -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - // Mocking FormattedDate and FormattedTime due to timezone differences on CI jest.mock('@kbn/i18n/react', () => { const original = jest.requireActual('@kbn/i18n/react'); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index f480e937f162a..dc568161d4fb4 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -17,11 +17,6 @@ import { DEFAULT_POLICY_SCHEDULE } from '../../public/application/constants'; const { setup } = pageHelpers.policyAdd; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - // mock for EuiSelectable's virtualization jest.mock('react-virtualized-auto-sizer', () => { return ({ diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts index 7eec80890ca86..a5b2ec73b85cd 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts @@ -18,11 +18,6 @@ const { setup: setupPolicyAdd } = pageHelpers.policyAdd; const EXPIRE_AFTER_VALUE = '5'; const EXPIRE_AFTER_UNIT = TIME_UNITS.MINUTE; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { let testBed: PolicyFormTestBed; let testBedPolicyAdd: PolicyFormTestBed; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts index 2a9e17a1c9c4d..e0c9aeffa09e8 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts @@ -11,16 +11,9 @@ import { RepositoryType } from '../../common/types'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { RepositoryAddTestBed } from './helpers/repository_add.helpers'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.repositoryAdd; const repositoryTypes = ['fs', 'url', 'source', 'azure', 'gcs', 's3', 'hdfs']; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { let testBed: RepositoryAddTestBed; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts index bab276584966b..1606db07b57b4 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts @@ -12,16 +12,9 @@ import { RepositoryEditTestSubjects } from './helpers/repository_edit.helpers'; import { RepositoryAddTestSubjects } from './helpers/repository_add.helpers'; import { REPOSITORY_EDIT } from './helpers/constant'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.repositoryEdit; const { setup: setupRepositoryAdd } = pageHelpers.repositoryAdd; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { let testBed: TestBed; let testBedRepositoryAdd: TestBed; diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts index 473a3392deb3e..e5d8e804691a8 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts @@ -4,55 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { deserializeSnapshotDetails } from './snapshot_serialization'; +import { deserializeSnapshotDetails, serializeSnapshotConfig } from './snapshot_serialization'; -describe('deserializeSnapshotDetails', () => { - test('deserializes a snapshot', () => { - expect( - deserializeSnapshotDetails( - 'repositoryName', - { - snapshot: 'snapshot name', - uuid: 'UUID', - version_id: 5, - version: 'version', - indices: ['index2', 'index3', 'index1'], - include_global_state: false, - state: 'SUCCESS', - start_time: '0', - start_time_in_millis: 0, - end_time: '1', - end_time_in_millis: 1, - duration_in_millis: 1, - shards: { - total: 3, - failed: 1, - successful: 2, - }, - failures: [ - { - index: 'z', - shard: 1, - }, - { - index: 'a', - shard: 3, - }, - { - index: 'a', - shard: 1, - }, - { - index: 'a', - shard: 2, - }, - ], - }, - 'found-snapshots', - [ +describe('Snapshot serialization and deserialization', () => { + describe('deserializeSnapshotDetails', () => { + test('deserializes a snapshot', () => { + expect( + deserializeSnapshotDetails( + 'repositoryName', { - snapshot: 'last_successful_snapshot', - uuid: 'last_successful_snapshot_UUID', + snapshot: 'snapshot name', + uuid: 'UUID', version_id: 5, version: 'version', indices: ['index2', 'index3', 'index1'], @@ -87,56 +49,109 @@ describe('deserializeSnapshotDetails', () => { }, ], }, - ] - ) - ).toEqual({ - repository: 'repositoryName', - snapshot: 'snapshot name', - uuid: 'UUID', - versionId: 5, - version: 'version', - // Indices are sorted. - indices: ['index1', 'index2', 'index3'], - dataStreams: [], - includeGlobalState: false, - // Failures are grouped and sorted by index, and the failures themselves are sorted by shard. - indexFailures: [ - { - index: 'a', - failures: [ + 'found-snapshots', + [ { - shard: 1, - }, - { - shard: 2, - }, - { - shard: 3, - }, - ], - }, - { - index: 'z', - failures: [ - { - shard: 1, + snapshot: 'last_successful_snapshot', + uuid: 'last_successful_snapshot_UUID', + version_id: 5, + version: 'version', + indices: ['index2', 'index3', 'index1'], + include_global_state: false, + state: 'SUCCESS', + start_time: '0', + start_time_in_millis: 0, + end_time: '1', + end_time_in_millis: 1, + duration_in_millis: 1, + shards: { + total: 3, + failed: 1, + successful: 2, + }, + failures: [ + { + index: 'z', + shard: 1, + }, + { + index: 'a', + shard: 3, + }, + { + index: 'a', + shard: 1, + }, + { + index: 'a', + shard: 2, + }, + ], }, - ], + ] + ) + ).toEqual({ + repository: 'repositoryName', + snapshot: 'snapshot name', + uuid: 'UUID', + versionId: 5, + version: 'version', + // Indices are sorted. + indices: ['index1', 'index2', 'index3'], + dataStreams: [], + includeGlobalState: false, + // Failures are grouped and sorted by index, and the failures themselves are sorted by shard. + indexFailures: [ + { + index: 'a', + failures: [ + { + shard: 1, + }, + { + shard: 2, + }, + { + shard: 3, + }, + ], + }, + { + index: 'z', + failures: [ + { + shard: 1, + }, + ], + }, + ], + state: 'SUCCESS', + startTime: '0', + startTimeInMillis: 0, + endTime: '1', + endTimeInMillis: 1, + durationInMillis: 1, + shards: { + total: 3, + failed: 1, + successful: 2, }, - ], - state: 'SUCCESS', - startTime: '0', - startTimeInMillis: 0, - endTime: '1', - endTimeInMillis: 1, - durationInMillis: 1, - shards: { - total: 3, - failed: 1, - successful: 2, - }, - managedRepository: 'found-snapshots', - isLastSuccessfulSnapshot: false, + managedRepository: 'found-snapshots', + isLastSuccessfulSnapshot: false, + }); + }); + }); + + describe('serializeSnapshotConfig', () => { + test('serializes config as expected', () => { + const metadata = { test: 'what have you' }; + expect(serializeSnapshotConfig({ metadata, indices: '.k*' })).toEqual({ + metadata, + indices: ['.k*'], + }); + }); + test('serializes empty config as expected', () => { + expect(serializeSnapshotConfig({})).toEqual({}); }); }); }); diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts index a85b49430eecd..61a57c9288f03 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts @@ -131,10 +131,10 @@ export function deserializeSnapshotConfig(snapshotConfigEs: SnapshotConfigEs): S export function serializeSnapshotConfig(snapshotConfig: SnapshotConfig): SnapshotConfigEs { const { indices, ignoreUnavailable, includeGlobalState, partial, metadata } = snapshotConfig; - const indicesArray = csvToArray(indices); + const maybeIndicesArray = csvToArray(indices); const snapshotConfigEs: SnapshotConfigEs = { - indices: indicesArray, + indices: maybeIndicesArray, ignore_unavailable: ignoreUnavailable, include_global_state: includeGlobalState, partial, diff --git a/x-pack/plugins/snapshot_restore/common/lib/utils.ts b/x-pack/plugins/snapshot_restore/common/lib/utils.ts index 96eb7cb6908d8..3d07aa8de3a55 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/utils.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/utils.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export const csvToArray = (indices?: string | string[]): string[] => { +export const csvToArray = (indices?: string | string[]): string[] | undefined => { return indices && Array.isArray(indices) ? indices : typeof indices === 'string' ? indices.split(',') - : []; + : undefined; }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts index 275915c5760af..7ec34dd7b3984 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts @@ -24,7 +24,7 @@ const maximumItemPreviewCount = 10; export const useCollapsibleList = ({ items }: Arg): ReturnValue => { const [isShowingFullList, setIsShowingFullList] = useState(false); - const itemsArray = csvToArray(items); + const itemsArray = csvToArray(items) ?? []; const displayItems: ChildItems = items === undefined ? 'all' diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx index ae22464c8a52b..407b9be14e3c1 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx @@ -35,11 +35,17 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }) => { const { retention = {} } = policy; - const updatePolicyRetention = (updatedFields: Partial): void => { + const updatePolicyRetention = ( + updatedFields: Partial, + validationHelperData = {} + ): void => { const newRetention = { ...retention, ...updatedFields }; - updatePolicy({ - retention: newRetention, - }); + updatePolicy( + { + retention: newRetention, + }, + validationHelperData + ); }; // State for touched inputs diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx index 94854905e6686..6f89427516453 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx @@ -25,7 +25,7 @@ import { import { SlmPolicyPayload } from '../../../../../../../../common/types'; import { useServices } from '../../../../../../app_context'; -import { PolicyValidation } from '../../../../../../services/validation'; +import { PolicyValidation, ValidatePolicyHelperData } from '../../../../../../services/validation'; import { orderDataStreamsAndIndices } from '../../../../../lib'; import { DataStreamBadge } from '../../../../../data_stream_badge'; @@ -34,12 +34,16 @@ import { mapSelectionToIndicesOptions, determineListMode } from './helpers'; import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text'; +interface IndicesConfig { + indices?: string[] | string; +} + interface Props { isManagedPolicy: boolean; policy: SlmPolicyPayload; indices: string[]; dataStreams: string[]; - onUpdate: (arg: { indices?: string[] | string }) => void; + onUpdate: (arg: IndicesConfig, validateHelperData: ValidatePolicyHelperData) => void; errors: PolicyValidation['errors']; } @@ -53,7 +57,7 @@ export const IndicesAndDataStreamsField: FunctionComponent = ({ dataStreams, indices, policy, - onUpdate, + onUpdate: _onUpdate, errors, }) => { const { i18n } = useServices(); @@ -66,6 +70,12 @@ export const IndicesAndDataStreamsField: FunctionComponent = ({ !config.indices || (Array.isArray(config.indices) && config.indices.length === 0) ); + const onUpdate = (data: IndicesConfig) => { + _onUpdate(data, { + validateIndicesCount: !isAllIndices, + }); + }; + const [indicesAndDataStreamsSelection, setIndicesAndDataStreamsSelection] = useState( () => Array.isArray(config.indices) && !isAllIndices diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx index 9d43c45d17ea7..f65156bada278 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -31,11 +31,17 @@ export const PolicyStepSettings: React.FunctionComponent = ({ }) => { const { config = {}, isManagedPolicy } = policy; - const updatePolicyConfig = (updatedFields: Partial): void => { + const updatePolicyConfig = ( + updatedFields: Partial, + validationHelperData = {} + ): void => { const newConfig = { ...config, ...updatedFields }; - updatePolicy({ - config: newConfig, - }); + updatePolicy( + { + config: newConfig, + }, + validationHelperData + ); }; const renderIgnoreUnavailableField = () => ( diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index d9fd4cca0d614..855abb04b7c2b 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -311,7 +311,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = }); } }} - selectedIndicesAndDataStreams={csvToArray(restoreIndices)} + selectedIndicesAndDataStreams={csvToArray(restoreIndices) ?? []} indices={snapshotIndices} dataStreams={snapshotDataStreams} /> diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/attempt_to_uri_decode.ts b/x-pack/plugins/snapshot_restore/public/application/lib/attempt_to_uri_decode.ts new file mode 100644 index 0000000000000..6f99fae118cd7 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/lib/attempt_to_uri_decode.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const attemptToURIDecode = (value: string) => { + let result: string; + + try { + result = decodeURI(value); + result = decodeURIComponent(result); + } catch (e1) { + try { + result = decodeURIComponent(value); + } catch (e2) { + result = value; + } + } + + return result; +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/index.ts b/x-pack/plugins/snapshot_restore/public/application/lib/index.ts new file mode 100644 index 0000000000000..c544df4365606 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/lib/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 { useDecodedParams } from './use_decoded_params'; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/use_decoded_params.ts b/x-pack/plugins/snapshot_restore/public/application/lib/use_decoded_params.ts new file mode 100644 index 0000000000000..d4582de4084ba --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/lib/use_decoded_params.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 { useParams } from 'react-router-dom'; +import { attemptToURIDecode } from './attempt_to_uri_decode'; + +export const useDecodedParams = < + Params extends { [K in keyof Params]?: string } = {} +>(): Params => { + const params = useParams>(); + const decodedParams = {} as Params; + + for (const [key, value] of Object.entries(params)) { + if (value) { + (decodedParams as any)[key] = attemptToURIDecode(value); + } + } + + return decodedParams; +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx index 962dfa73e95c7..12e7e2de9383d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx @@ -89,7 +89,7 @@ export const SnapshotRestoreHome: React.FunctionComponent { - history.push(`${BASE_PATH}/${newSection}`); + history.push(encodeURI(`${BASE_PATH}/${encodeURIComponent(newSection)}`)); }; // Set breadcrumb and page title diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx index 5959ad6441f5d..f67e8eb586238 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx @@ -24,6 +24,8 @@ import { EuiSpacer, } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; + import { SlmPolicy } from '../../../../../../common/types'; import { useServices } from '../../../../app_context'; import { SectionError, Error } from '../../../../../shared_imports'; @@ -41,8 +43,6 @@ import { } from '../../../../components'; import { TabSummary, TabHistory } from './tabs'; -import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; - interface Props { policyName: SlmPolicy['name']; onClose: () => void; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx index 39ef66eb0658c..655bd0e9d8bb9 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx @@ -9,6 +9,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiEmptyPrompt, EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; + import { SectionError, Error, @@ -20,6 +22,7 @@ import { SlmPolicy } from '../../../../../common/types'; import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common'; import { SectionLoading } from '../../../components'; import { BASE_PATH, UIM_POLICY_LIST_LOAD } from '../../../constants'; +import { useDecodedParams } from '../../../lib'; import { useLoadPolicies, useLoadRetentionSettings } from '../../../services/http'; import { linkToAddPolicy, linkToPolicy } from '../../../services/navigation'; import { useServices } from '../../../app_context'; @@ -28,18 +31,14 @@ import { PolicyDetails } from './policy_details'; import { PolicyTable } from './policy_table'; import { PolicyRetentionSchedule } from './policy_retention_schedule'; -import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; - interface MatchParams { policyName?: SlmPolicy['name']; } export const PolicyList: React.FunctionComponent> = ({ - match: { - params: { policyName }, - }, history, }) => { + const { policyName } = useDecodedParams(); const { error, isLoading, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx index f6b9ac08cc0a2..9afdad3806def 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx @@ -7,11 +7,14 @@ import React, { Fragment, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { RouteComponentProps } from 'react-router-dom'; - import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; + +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; + import { Repository } from '../../../../../common/types'; import { SectionError, Error } from '../../../../shared_imports'; import { SectionLoading } from '../../../components'; +import { useDecodedParams } from '../../../lib'; import { BASE_PATH, UIM_REPOSITORY_LIST_LOAD } from '../../../constants'; import { useServices } from '../../../app_context'; import { useLoadRepositories } from '../../../services/http'; @@ -20,18 +23,14 @@ import { linkToAddRepository, linkToRepository } from '../../../services/navigat import { RepositoryDetails } from './repository_details'; import { RepositoryTable } from './repository_table'; -import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; - interface MatchParams { repositoryName?: Repository['name']; } export const RepositoryList: React.FunctionComponent> = ({ - match: { - params: { repositoryName }, - }, history, }) => { + const { repositoryName } = useDecodedParams(); const { error, isLoading, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx index 2b8df7294c374..d13188fc44730 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx @@ -24,6 +24,7 @@ import { linkToSnapshot, } from '../../../services/navigation'; import { useServices } from '../../../app_context'; +import { useDecodedParams } from '../../../lib'; import { SnapshotDetails } from './snapshot_details'; import { SnapshotTable } from './snapshot_table'; @@ -35,12 +36,10 @@ interface MatchParams { } export const SnapshotList: React.FunctionComponent> = ({ - match: { - params: { repositoryName, snapshotId }, - }, location: { search }, history, }) => { + const { repositoryName, snapshotId } = useDecodedParams(); const { error, isLoading, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx index 90cd26c821c5e..dead9abb6ef0c 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx @@ -43,7 +43,7 @@ export const PolicyAdd: React.FunctionComponent = ({ if (error) { setSaveError(error); } else { - history.push(`${BASE_PATH}/policies/${name}`); + history.push(encodeURI(`${BASE_PATH}/policies/${encodeURIComponent(name)}`)); } }; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx index 915b1cbef1233..7af663b29957d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx @@ -10,6 +10,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle, EuiCallOut } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../common/types'; import { SectionError, Error } from '../../../shared_imports'; +import { useDecodedParams } from '../../lib'; import { TIME_UNITS } from '../../../../common/constants'; import { SectionLoading, PolicyForm } from '../../components'; import { BASE_PATH } from '../../constants'; @@ -22,12 +23,10 @@ interface MatchParams { } export const PolicyEdit: React.FunctionComponent> = ({ - match: { - params: { name }, - }, history, location: { pathname }, }) => { + const { name } = useDecodedParams(); const { i18n } = useServices(); // Set breadcrumb and page title @@ -83,12 +82,12 @@ export const PolicyEdit: React.FunctionComponent { - history.push(`${BASE_PATH}/policies/${name}`); + history.push(encodeURI(`${BASE_PATH}/policies/${encodeURIComponent(name)}`)); }; const renderLoading = () => { diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx index 08bfde833c368..a66d137471eba 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx @@ -44,7 +44,11 @@ export const RepositoryAdd: React.FunctionComponent = ({ } else { const { redirect } = parse(search.replace(/^\?/, ''), { sort: false }); - history.push(redirect ? (redirect as string) : `${BASE_PATH}/${section}/${name}`); + history.push( + redirect + ? (redirect as string) + : encodeURI(`${BASE_PATH}/${encodeURIComponent(section)}/${encodeURIComponent(name)}`) + ); } }; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx index 95f8b9b8bde7d..3e8cff5793572 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx @@ -16,18 +16,17 @@ import { BASE_PATH, Section } from '../../constants'; import { useServices } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { editRepository, useLoadRepository } from '../../services/http'; +import { useDecodedParams } from '../../lib'; interface MatchParams { name: string; } export const RepositoryEdit: React.FunctionComponent> = ({ - match: { - params: { name }, - }, history, }) => { const { i18n } = useServices(); + const { name } = useDecodedParams(); const section = 'repositories' as Section; // Set breadcrumb and page title @@ -70,7 +69,9 @@ export const RepositoryEdit: React.FunctionComponent> = ({ - match: { - params: { repositoryName, snapshotId }, - }, history, }) => { const { i18n } = useServices(); + const { repositoryName, snapshotId } = useDecodedParams(); // Set breadcrumb and page title useEffect(() => { diff --git a/x-pack/plugins/snapshot_restore/public/application/services/navigation/links.ts b/x-pack/plugins/snapshot_restore/public/application/services/navigation/links.ts index 503704c6fe820..b498dc280d395 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/navigation/links.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/navigation/links.ts @@ -13,33 +13,37 @@ export function linkToRepositories() { } export function linkToRepository(repositoryName: string) { - return `/repositories/${encodeURIComponent(repositoryName)}`; + return encodeURI(`/repositories/${encodeURIComponent(repositoryName)}`); } export function linkToEditRepository(repositoryName: string) { - return `/edit_repository/${encodeURIComponent(repositoryName)}`; + return encodeURI(`/edit_repository/${encodeURIComponent(repositoryName)}`); } export function linkToAddRepository(redirect?: string) { - return `/add_repository${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''}`; + return encodeURI(`/add_repository${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''}`); } export function linkToSnapshots(repositoryName?: string, policyName?: string) { if (repositoryName) { - return `/snapshots?repository=${encodeURIComponent(repositoryName)}`; + return encodeURI(`/snapshots?repository=${encodeURIComponent(repositoryName)}`); } if (policyName) { - return `/snapshots?policy=${encodeURIComponent(policyName)}`; + return encodeURI(`/snapshots?policy=${encodeURIComponent(policyName)}`); } return `/snapshots`; } export function linkToSnapshot(repositoryName: string, snapshotName: string) { - return `/snapshots/${encodeURIComponent(repositoryName)}/${encodeURIComponent(snapshotName)}`; + return encodeURI( + `/snapshots/${encodeURIComponent(repositoryName)}/${encodeURIComponent(snapshotName)}` + ); } export function linkToRestoreSnapshot(repositoryName: string, snapshotName: string) { - return `/restore/${encodeURIComponent(repositoryName)}/${encodeURIComponent(snapshotName)}`; + return encodeURI( + `/restore/${encodeURIComponent(repositoryName)}/${encodeURIComponent(snapshotName)}` + ); } export function linkToPolicies() { @@ -47,11 +51,11 @@ export function linkToPolicies() { } export function linkToPolicy(policyName: string) { - return `/policies/${encodeURIComponent(policyName)}`; + return encodeURI(`/policies/${encodeURIComponent(policyName)}`); } export function linkToEditPolicy(policyName: string) { - return `/edit_policy/${encodeURIComponent(policyName)}`; + return encodeURI(`/edit_policy/${encodeURIComponent(policyName)}`); } export function linkToAddPolicy() { diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/index.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/index.ts index 7fd755497eec6..82bf0ef06c400 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/validation/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/index.ts @@ -12,4 +12,4 @@ export { export { RestoreValidation, validateRestore } from './validate_restore'; -export { PolicyValidation, validatePolicy } from './validate_policy'; +export { PolicyValidation, validatePolicy, ValidatePolicyHelperData } from './validate_policy'; diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts index 24960b2533230..4314b703722f6 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts @@ -25,21 +25,32 @@ const isSnapshotNameNotLowerCase = (str: string): boolean => { return strExcludeDate !== strExcludeDate.toLowerCase() ? true : false; }; +export interface ValidatePolicyHelperData { + managedRepository?: { + name: string; + policy: string; + }; + isEditing?: boolean; + policyName?: string; + /** + * Whether to block on the indices configured for this snapshot. + * + * By default ES will back up all indices and data streams if this is an empty array or left blank. + * However, in the UI, under certain conditions, like when displaying indices to select for backup, + * we want to block users from submitting an empty array, but not block the entire form if they + * are not configuring this value - like when they are on a previous step. + */ + validateIndicesCount?: boolean; +} + export const validatePolicy = ( policy: SlmPolicyPayload, - validationHelperData: { - managedRepository?: { - name: string; - policy: string; - }; - isEditing?: boolean; - policyName?: string; - } + validationHelperData: ValidatePolicyHelperData ): PolicyValidation => { const i18n = textService.i18n; const { name, snapshotName, schedule, repository, config, retention } = policy; - const { managedRepository, isEditing, policyName } = validationHelperData; + const { managedRepository, isEditing, policyName, validateIndicesCount } = validationHelperData; const validation: PolicyValidation = { isValid: true, @@ -96,7 +107,12 @@ export const validatePolicy = ( ); } - if (config && typeof config.indices === 'string' && config.indices.trim().length === 0) { + if ( + validateIndicesCount && + config && + typeof config.indices === 'string' && + config.indices.trim().length === 0 + ) { validation.errors.indices.push( i18n.translate('xpack.snapshotRestore.policyValidation.indexPatternRequiredErrorMessage', { defaultMessage: 'At least one index pattern is required.', @@ -104,7 +120,12 @@ export const validatePolicy = ( ); } - if (config && Array.isArray(config.indices) && config.indices.length === 0) { + if ( + validateIndicesCount && + config && + Array.isArray(config.indices) && + config.indices.length === 0 + ) { validation.errors.indices.push( i18n.translate('xpack.snapshotRestore.policyValidation.indicesRequiredErrorMessage', { defaultMessage: 'You must select at least one data stream or index.', 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 dc07044ce8ed7..eacb1febd20ff 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -253,31 +253,6 @@ "charts.controls.rangeErrorMessage": "値は{min}と{max}の間でなければなりません", "charts.controls.vislibBasicOptions.legendPositionLabel": "凡例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", - "common.ui.flotCharts.aprLabel": "4 月", - "common.ui.flotCharts.augLabel": "8 月", - "common.ui.flotCharts.decLabel": "12 月", - "common.ui.flotCharts.febLabel": "2 月", - "common.ui.flotCharts.friLabel": "金", - "common.ui.flotCharts.janLabel": "1 月", - "common.ui.flotCharts.julLabel": "7 月", - "common.ui.flotCharts.junLabel": "6 月", - "common.ui.flotCharts.marLabel": "3 月", - "common.ui.flotCharts.mayLabel": "5 月", - "common.ui.flotCharts.monLabel": "月", - "common.ui.flotCharts.novLabel": "11 月", - "common.ui.flotCharts.octLabel": "10 月", - "common.ui.flotCharts.pie.unableToDrawLabelsInsideCanvasErrorMessage": "キャンバス内のラベルではパイを作成できません", - "common.ui.flotCharts.satLabel": "土", - "common.ui.flotCharts.sepLabel": "9 月", - "common.ui.flotCharts.sunLabel": "日", - "common.ui.flotCharts.thuLabel": "木", - "common.ui.flotCharts.tueLabel": "火", - "common.ui.flotCharts.wedLabel": "水", - "common.ui.stateManagement.unableToParseUrlErrorMessage": "URL をパースできません", - "common.ui.stateManagement.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", - "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", - "common.ui.url.replacementFailedErrorMessage": "置換に失敗、未解決の表現式: {expr}", - "common.ui.url.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", "console.autocomplete.addMethodMetaText": "メソド", "console.consoleDisplayName": "コンソール", "console.consoleMenu.copyAsCurlMessage": "リクエストが URL としてコピーされました", @@ -526,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": "既存のユーザーを追加", @@ -739,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": "インプットの終わり", @@ -1115,7 +1047,6 @@ "data.search.aggs.metrics.count.customLabel.help": "このアグリゲーションのカスタムラベルを表します", "data.search.aggs.metrics.count.enabled.help": "このアグリゲーションが有効かどうかを指定します", "data.search.aggs.metrics.count.id.help": "このアグリゲーションのID", - "data.search.aggs.metrics.count.json.help": "アグリゲーションがElasticsearchに送信されるときに含める高度なJSON", "data.search.aggs.metrics.count.schema.help": "このアグリゲーションで使用するスキーマ", "data.search.aggs.metrics.countLabel": "カウント", "data.search.aggs.metrics.countTitle": "カウント", @@ -1672,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 コマース] コントロール", @@ -1703,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": "[フライト] フライト遅延合計", @@ -1718,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トラフィックログデータを分析します", @@ -4459,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": "読み取り専用", @@ -4559,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": "しきい値を超えたグループ。", @@ -4705,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}]の異常検知ジョブが正常に作成されました。機械学習がトラフィック異常値の分析を開始するには、少し時間がかかります。", @@ -4752,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": "未対応", @@ -4779,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", @@ -4858,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": "例外のスタックトレース", @@ -4901,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": "中央", @@ -4909,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": "ズームアウト", @@ -4958,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でジョブを表示", @@ -5068,7 +5002,6 @@ "xpack.apm.transactionActionMenu.viewInUptime": "ステータス", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "サンプルドキュメントを表示", "xpack.apm.transactionBreakdown.chartTitle": "スパンタイプ別時間", - "xpack.apm.transactionBreakdown.noData": "この時間範囲のデータがありません。", "xpack.apm.transactionCardinalityWarning.body": "一意のトランザクション名の数が構成された値{bucketSize}を超えています。エージェントを再構成し、類似したトランザクションをグループ化するか、{codeBlock}の値を増やしてください。", "xpack.apm.transactionCardinalityWarning.docsLink": "詳細はドキュメントをご覧ください", "xpack.apm.transactionCardinalityWarning.title": "このビューには、報告されたトランザクションのサブセットが表示されます。", @@ -5433,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": "レンダリングされたワークパッドをダウンロードできませんでした", @@ -6320,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": "アセットの管理", @@ -6773,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を再起動して、前のステップから構成変更を取得します。", @@ -8127,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": "最大インデックスサイズが必要です。", @@ -8266,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": "ポリシーを作成", @@ -8491,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ジョブが作成されました。現在の構成を適用するにはジョブを再作成してください。これにより以前検出された異常が削除されます。", @@ -8506,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": "この機能には機械学習が必要です", @@ -8526,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": "フィルターを調整してみてください。", @@ -8556,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": "機械学習機能を使用するには、プラチナサブスクリプションが必要です。", @@ -8755,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": "条件", @@ -8771,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": "インベントリタイプ", @@ -8797,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": "より小", @@ -8819,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": "読み込み中", @@ -8837,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": "/", @@ -8929,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": "列を追加", @@ -9080,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": "メタデータ", @@ -9141,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エージェントをダウンロード", @@ -9173,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": "実行中", @@ -9199,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": "エージェントを読み込み中...", @@ -9223,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": "ベータリリース", @@ -9239,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": "フリート", @@ -9249,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": "統合", @@ -9363,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": "統合", @@ -9508,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": "名前", @@ -9519,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": "エージェントが収集するデータはさまざまなデータセットに整理されます。", @@ -9542,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アクセス権の確認中に問題が発生しました", @@ -9591,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": "サーバーからユーザー特権を取得中にエラーが発生。", @@ -9716,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": "バイト", @@ -9764,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が無効です。", @@ -9803,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": "データテーブル", @@ -9849,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} の平均", @@ -9876,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": "この関数を使用するには、フィールドを選択してください。", @@ -9884,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": "グループ分けの条件", @@ -10190,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": "境界ソース", @@ -10519,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": "レイヤーがタイルに存在するズームレベル。これは直接可視性に対応しません。レベルが低いレイヤーデータは常に高いズームレベルで表示できます(ただし逆はありません)。", @@ -10661,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": "アクセスが拒否されました", @@ -10937,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です。", @@ -10986,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": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", @@ -11001,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": "このタイトルのインデックスパターンが既に存在します。", @@ -11046,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": "学習で使用されるドキュメントを選択するために使用される乱数生成器のシード", @@ -11129,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": "削除するにはデータフレーム分析ジョブを停止してください。", @@ -11140,7 +10899,6 @@ "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage": "インデックスパターン{destinationIndex}を削除する要求が確認されました。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage": "ディスティネーションインデックス{destinationIndex}を削除する要求が確認されました。", "xpack.ml.dataframe.analyticsList.deleteDestinationIndexTitle": "ディスティネーションインデックス{indexName}を削除", - "xpack.ml.dataframe.analyticsList.deleteModalBody": "この分析ジョブを削除してよろしいですか?", "xpack.ml.dataframe.analyticsList.deleteModalCancelButton": "キャンセル", "xpack.ml.dataframe.analyticsList.deleteModalDeleteButton": "削除", "xpack.ml.dataframe.analyticsList.deleteModalTitle": "{analyticsId}の削除", @@ -11199,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": "探索", @@ -11278,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": "異常", @@ -11407,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": "適用", @@ -11635,7 +11399,6 @@ "xpack.ml.jobsList.deleteJobModal.cancelButtonLabel": "キャンセル", "xpack.ml.jobsList.deleteJobModal.closeButtonLabel": "閉じる", "xpack.ml.jobsList.deleteJobModal.deleteButtonLabel": "削除", - "xpack.ml.jobsList.deleteJobModal.deleteJobsDescription": "{jobsCount, plural, one {このジョブ} other {これらのジョブ}}を削除してよろしいですか?", "xpack.ml.jobsList.deleteJobModal.deleteJobsTitle": "{jobsCount, plural, one {{jobId}} other {# 件のジョブ}}を削除", "xpack.ml.jobsList.deleteJobModal.deleteMultipleJobsDescription": "{jobsCount, plural, one {ジョブ} other {複数ジョブ}}の削除には時間がかかる場合があります。{jobsCount, plural, one {} other {}}バックグラウンドで削除され、ジョブリストからすぐに消えない場合があります", "xpack.ml.jobsList.deleteJobModal.deletingJobsStatusLabel": "ジョブを削除中", @@ -11925,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": "(既に存在します)", @@ -12304,7 +12068,6 @@ "xpack.ml.ruleEditor.deleteJobRule.ruleNoLongerExistsErrorMessage": "ジョブ {jobId} の検知器インデックス {detectorIndex} のルールが現在存在しません", "xpack.ml.ruleEditor.deleteRuleModal.cancelButtonLabel": "キャンセル", "xpack.ml.ruleEditor.deleteRuleModal.deleteButtonLabel": "削除", - "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleDescription": "このルールを削除してよろしいですか?", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleLinkText": "ルールを削除", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleTitle": "ルールの削除", "xpack.ml.ruleEditor.detectorDescriptionList.detectorTitle": "検知器", @@ -12398,7 +12161,6 @@ "xpack.ml.settings.filterLists.deleteFilterListModal.cancelButtonLabel": "キャンセル", "xpack.ml.settings.filterLists.deleteFilterListModal.confirmButtonLabel": "削除", "xpack.ml.settings.filterLists.deleteFilterListModal.deleteButtonLabel": "削除", - "xpack.ml.settings.filterLists.deleteFilterListModal.deleteWarningMessage": "{selectedFilterListsLength, plural, one {このフィルダー} other {これらのフィルター}}を削除してよろしいですか?", "xpack.ml.settings.filterLists.deleteFilterListModal.modalTitle": "{selectedFilterListsLength, plural, one {{selectedFilterId}} other {# フィルターリスト}}の削除", "xpack.ml.settings.filterLists.deleteFilterLists.deletingErrorMessage": "フィルターリスト {filterListId} の削除中にエラーが発生しました。{respMessage}", "xpack.ml.settings.filterLists.deleteFilterLists.deletingNotificationMessage": "{filterListsToDeleteLength, plural, one {{filterListToDeleteId}} other {# フィルターリスト}}を削除しています", @@ -12468,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": "異常", @@ -12720,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}」が再起動しました。", @@ -13001,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": "空きディスク容量", @@ -13103,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月", @@ -13965,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のインストール", @@ -13977,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": "新機能", @@ -15038,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": "リスクスコア無効化", @@ -15046,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": "デフォルト重要度", @@ -15382,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": "タイトルが必要です。", @@ -15396,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": "コピー", @@ -15457,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機械学習ジョブが見つかりませんでした", @@ -15535,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": "タイムラインで調査", @@ -15582,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}", @@ -15605,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": "すべての生成されたアラートを「基本」アラートに設定", @@ -15627,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": "ルール調査ガイドを追加...", @@ -15635,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": "低", @@ -15653,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": "ジョブを選択してください", @@ -15686,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": "閉じる", @@ -15695,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": "ドキュメンテーションを表示", @@ -15986,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": "検出エンジンを設定しましょう", @@ -16077,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 {個のルール}}をインストール ", @@ -16108,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": "クリップボードにコピー", @@ -16129,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", @@ -16209,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", @@ -16238,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": "新しいポリシーを作成", @@ -16264,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": "以下のビジュアライゼーションとイベントリストの一部のプロセスイベントを表示できませんでした。データの上限に達しました。", @@ -16277,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": "イベントを待機しています...", @@ -16308,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", @@ -16375,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": "存在する", @@ -16424,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": "このルールには例外がありません", @@ -16435,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": "セキュリティ", @@ -16483,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": "前回の認識", @@ -16504,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": "すべてのホスト", @@ -16517,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": "前回の認識", @@ -16562,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": "値リストの削除", @@ -16776,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": "フィードバックをオンラインで送信", @@ -16823,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": "表示中", @@ -16929,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": "新しいタイムラインを作成するときにデフォルトで提供されるタイムライン。", @@ -16951,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": "説明", @@ -17860,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つまたは複数の矛盾が検出されました。解決するにはこのセクションを拡張してください。", @@ -17869,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": "カスタム画像", @@ -18359,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 144bc1cac1852..bd30703dd5bd6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -253,31 +253,6 @@ "charts.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", "charts.controls.vislibBasicOptions.legendPositionLabel": "图例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", - "common.ui.flotCharts.aprLabel": "四月", - "common.ui.flotCharts.augLabel": "八月", - "common.ui.flotCharts.decLabel": "十二月", - "common.ui.flotCharts.febLabel": "二月", - "common.ui.flotCharts.friLabel": "周五", - "common.ui.flotCharts.janLabel": "一月", - "common.ui.flotCharts.julLabel": "七月", - "common.ui.flotCharts.junLabel": "六月", - "common.ui.flotCharts.marLabel": "三月", - "common.ui.flotCharts.mayLabel": "五月", - "common.ui.flotCharts.monLabel": "周一", - "common.ui.flotCharts.novLabel": "十一月", - "common.ui.flotCharts.octLabel": "十月", - "common.ui.flotCharts.pie.unableToDrawLabelsInsideCanvasErrorMessage": "无法用画布内包含的标签绘制饼图", - "common.ui.flotCharts.satLabel": "周六", - "common.ui.flotCharts.sepLabel": "九月", - "common.ui.flotCharts.sunLabel": "周日", - "common.ui.flotCharts.thuLabel": "周四", - "common.ui.flotCharts.tueLabel": "周二", - "common.ui.flotCharts.wedLabel": "周三", - "common.ui.stateManagement.unableToParseUrlErrorMessage": "无法解析 URL", - "common.ui.stateManagement.unableToRestoreUrlErrorMessage": "无法完整还原 URL,确保使用共享功能。", - "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", - "common.ui.url.replacementFailedErrorMessage": "替换失败,未解析的表达式:{expr}", - "common.ui.url.savedObjectIsMissingNotificationMessage": "已保存对象缺失", "console.autocomplete.addMethodMetaText": "方法", "console.consoleDisplayName": "控制台", "console.consoleMenu.copyAsCurlMessage": "请求已复制为 cURL", @@ -526,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": "将现有", @@ -739,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": "输入结束", @@ -1116,7 +1048,6 @@ "data.search.aggs.metrics.count.customLabel.help": "表示此聚合的定制标签", "data.search.aggs.metrics.count.enabled.help": "指定是否启用此聚合", "data.search.aggs.metrics.count.id.help": "此聚合的 ID", - "data.search.aggs.metrics.count.json.help": "聚合发送至 Elasticsearch 时要包括的高级 json", "data.search.aggs.metrics.count.schema.help": "要用于此聚合的方案", "data.search.aggs.metrics.countLabel": "计数", "data.search.aggs.metrics.countTitle": "计数", @@ -1673,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": "[电子商务] 控制", @@ -1704,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": "[航班] 航班延误总数", @@ -1719,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 流量日志数据", @@ -4460,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": "只读", @@ -4560,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": "超过阈值的组。", @@ -4706,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} 筛选", @@ -4753,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": "未处理", @@ -4780,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", @@ -4903,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": "中", @@ -4911,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": "缩小", @@ -5070,7 +5005,6 @@ "xpack.apm.transactionActionMenu.viewInUptime": "状态", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "查看样例文档", "xpack.apm.transactionBreakdown.chartTitle": "跨度类型花费的时间", - "xpack.apm.transactionBreakdown.noData": "此时间范围内没有数据。", "xpack.apm.transactionCardinalityWarning.body": "唯一事务名称的数目超过 {bucketSize} 的已配置值。尝试重新配置您的代理以对类似的事务分组或增大 {codeBlock} 的值", "xpack.apm.transactionCardinalityWarning.docsLink": "在文档中了解详情", "xpack.apm.transactionCardinalityWarning.title": "此视图显示已报告事务的子集。", @@ -5435,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", @@ -6323,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": "管理资产", @@ -6775,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 以应用上一步骤中的配置更改。", @@ -8130,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": "最大索引大小必填。", @@ -8269,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": "创建策略", @@ -8494,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 作业时所使用的源配置不同。重新创建作业以应用当前配置。这将移除以前检测到的异常。", @@ -8509,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", @@ -8559,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 功能。", @@ -8758,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": "条件", @@ -8774,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": "库存类型", @@ -8800,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": "低于", @@ -8822,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": "正在加载", @@ -8840,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": "的", @@ -8932,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": "添加列", @@ -9083,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": "元数据", @@ -9144,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 代理", @@ -9176,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": "正在运行", @@ -9202,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": "正在加载代理……", @@ -9226,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": "公测版", @@ -9242,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", @@ -9252,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": "集成", @@ -9366,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": "集成", @@ -9511,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": "名称", @@ -9522,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": "您的代理收集的数据组织到各种数据集中。", @@ -9545,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 权限时出现问题", @@ -9594,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": "从服务器获取用户权限时出错。", @@ -9719,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": "字节", @@ -9735,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": "失败", @@ -9743,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", @@ -9767,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 无效。", @@ -9806,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": "数据表", @@ -9852,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} 的平均值", @@ -9879,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": "要使用此函数,请选择字段。", @@ -9887,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": "分组依据", @@ -10193,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": "边界源", @@ -10522,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": "磁帖中存在该图层的缩放级别。这不直接对应于可见性。较低级别的图层数据始终可以显示在较高缩放级别(反之却不然)。", @@ -10664,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": "访问被拒绝", @@ -10940,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。", @@ -10989,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": "获取现有索引模式标题时发生错误:", @@ -11049,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": "用于选取哪个文档用于训练的随机生成器种子", @@ -11132,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": "停止数据帧分析作业,以便将其删除。", @@ -11143,7 +10905,6 @@ "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage": "删除索引模式 {destinationIndex} 的请求已确认。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage": "删除目标索引 {destinationIndex} 的请求已确认。", "xpack.ml.dataframe.analyticsList.deleteDestinationIndexTitle": "删除目标索引 {indexName}", - "xpack.ml.dataframe.analyticsList.deleteModalBody": "是否确定要删除此分析作业?", "xpack.ml.dataframe.analyticsList.deleteModalCancelButton": "取消", "xpack.ml.dataframe.analyticsList.deleteModalDeleteButton": "删除", "xpack.ml.dataframe.analyticsList.deleteModalTitle": "删除 {analyticsId}", @@ -11203,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": "探查", @@ -11282,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": "异常", @@ -11411,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": "应用", @@ -11639,7 +11406,6 @@ "xpack.ml.jobsList.deleteJobModal.cancelButtonLabel": "取消", "xpack.ml.jobsList.deleteJobModal.closeButtonLabel": "关闭", "xpack.ml.jobsList.deleteJobModal.deleteButtonLabel": "删除", - "xpack.ml.jobsList.deleteJobModal.deleteJobsDescription": "是否确定要删除{jobsCount, plural, one {此作业} other {这些作业}}?", "xpack.ml.jobsList.deleteJobModal.deleteJobsTitle": "删除 {jobsCount, plural, one {{jobId}} other {# 个作业}}", "xpack.ml.jobsList.deleteJobModal.deleteMultipleJobsDescription": "删除{jobsCount, plural, one {一个作业} other {多个作业}}会非常耗时。将在后台删除{jobsCount, plural, one {该作业} other {这些作业}},但删除的作业可能不会立即从作业列表中消失", "xpack.ml.jobsList.deleteJobModal.deletingJobsStatusLabel": "正在删除作业", @@ -11929,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": "(已存在)", @@ -12308,7 +12075,6 @@ "xpack.ml.ruleEditor.deleteJobRule.ruleNoLongerExistsErrorMessage": "作业 {jobId} 中不再存在检测工具索引 {detectorIndex} 的规则", "xpack.ml.ruleEditor.deleteRuleModal.cancelButtonLabel": "取消", "xpack.ml.ruleEditor.deleteRuleModal.deleteButtonLabel": "删除", - "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleDescription": "是否确定要删除此规则?", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleLinkText": "删除规则", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleTitle": "删除规则", "xpack.ml.ruleEditor.detectorDescriptionList.detectorTitle": "检测工具", @@ -12402,7 +12168,6 @@ "xpack.ml.settings.filterLists.deleteFilterListModal.cancelButtonLabel": "取消", "xpack.ml.settings.filterLists.deleteFilterListModal.confirmButtonLabel": "删除", "xpack.ml.settings.filterLists.deleteFilterListModal.deleteButtonLabel": "删除", - "xpack.ml.settings.filterLists.deleteFilterListModal.deleteWarningMessage": "是否确定要删除{selectedFilterListsLength, plural, one {此筛选列表} other {这些筛选列表}}", "xpack.ml.settings.filterLists.deleteFilterListModal.modalTitle": "删除 {selectedFilterListsLength, plural, one {{selectedFilterId}} other {# 个筛选列表}}", "xpack.ml.settings.filterLists.deleteFilterLists.deletingErrorMessage": "删除筛选列表 {filterListId} 时出错。{respMessage}", "xpack.ml.settings.filterLists.deleteFilterLists.deletingNotificationMessage": "正在删除 {filterListsToDeleteLength, plural, one {{filterListToDeleteId}} other {# 个筛选列表}}", @@ -12472,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": "异常", @@ -12726,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}”已重新启动。", @@ -13007,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": "可用磁盘空间", @@ -13109,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": "六月", @@ -13971,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", @@ -13983,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": "最近的新闻", @@ -15044,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": "风险分数覆盖", @@ -15052,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": "默认严重性", @@ -15385,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": "标题必填。", @@ -15402,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": "复制", @@ -15463,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 作业", @@ -15541,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": "在时间线中调查", @@ -15588,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}", @@ -15611,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": "将所有生成的告警标记为“构建块”告警", @@ -15633,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": "添加规则调查指南......", @@ -15641,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": "低", @@ -15659,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": "选择作业", @@ -15692,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": "关闭", @@ -15701,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": "查看文档", @@ -15992,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": "让我们来设置您的检测引擎", @@ -16083,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 {规则}} ", @@ -16114,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": "复制到剪贴板", @@ -16137,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", @@ -16215,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", @@ -16245,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": "创建新策略", @@ -16271,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": "下面可视化和事件列表中的一些进程事件无法显示,因为已达到数据限制。", @@ -16284,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": "等候事件......", @@ -16315,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": "公测版", @@ -16382,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": "存在", @@ -16431,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": "此规则没有例外", @@ -16442,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": "安全", @@ -16490,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": "最后看到时间", @@ -16511,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": "所有主机", @@ -16524,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": "最后看到时间", @@ -16569,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": "删除值列表", @@ -16783,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": "在线提交反馈", @@ -16830,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": "正在显示", @@ -16936,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": "创建新的时间线时默认提供的时间线。", @@ -16958,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": "描述", @@ -17867,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} 工作区中检测到一个或多个冲突。展开此部分以进行解决。", @@ -17876,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": "定制图像", @@ -18366,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/es_index/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx index 417a9e09086a2..b56ae35df5d06 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx @@ -72,17 +72,31 @@ describe('index connector validation with minimal config', () => { describe('action params validation', () => { test('action params validation succeeds when action params is valid', () => { const actionParams = { - documents: ['test'], + documents: [{ test: 1234 }], }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: {}, + errors: { + documents: [], + }, }); const emptyActionParams = {}; expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({ - errors: {}, + errors: { + documents: ['Document is required and should be a valid JSON object.'], + }, + }); + + const invalidDocumentActionParams = { + documents: [{}], + }; + + expect(actionTypeModel.validateParams(invalidDocumentActionParams)).toEqual({ + errors: { + documents: ['Document is required and should be a valid JSON object.'], + }, }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx index 3ee663a5fc8a0..c0255650e0f37 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx @@ -44,8 +44,23 @@ export function getActionType(): ActionTypeModel import('./es_index_connector')), actionParamsFields: lazy(() => import('./es_index_params')), - validateParams: (): ValidationResult => { - return { errors: {} }; + validateParams: (actionParams: IndexActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + documents: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.documents?.length || Object.keys(actionParams.documents[0]).length === 0) { + errors.documents.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson', + { + defaultMessage: 'Document is required and should be a valid JSON object.', + } + ) + ); + } + return validationResult; }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx index e8e8cc582512e..495707db4975c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -17,6 +17,7 @@ export const IndexParamsFields = ({ editAction, messageVariables, docLinks, + errors, }: ActionParamsProps) => { const { documents } = actionParams; @@ -24,8 +25,10 @@ export const IndexParamsFields = ({ try { const documentsJSON = JSON.parse(updatedDocuments); editAction('documents', [documentsJSON], index); - // eslint-disable-next-line no-empty - } catch (e) {} + } catch (e) { + // set document as empty to turn on the validation for non empty valid JSON object + editAction('documents', [{}], index); + } }; return ( @@ -34,7 +37,7 @@ export const IndexParamsFields = ({ messageVariables={messageVariables} paramsProperty={'documents'} inputTargetValue={ - documents && documents.length > 0 ? ((documents[0] as unknown) as string) : '' + documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined } label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel', @@ -48,6 +51,7 @@ export const IndexParamsFields = ({ defaultMessage: 'Code editor', } )} + errors={errors.documents as string[]} onDocumentsChange={onDocumentsChange} helpText={ } + onBlur={() => { + if ( + !(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined) + ) { + // set document as empty to turn on the validation for non empty valid JSON object + onDocumentsChange('{}'); + } + }} /> ); 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/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx index 1dfd9e3edc2c5..ff9ad936f224f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx @@ -21,7 +21,7 @@ const WebhookParamsFields: React.FunctionComponent { editAction('body', json, index); }} + onBlur={() => { + if (!body) { + editAction('body', '', index); + } + }} /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx index 0b8184fc441fd..5ea15deb53161 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx @@ -14,12 +14,13 @@ import { ActionVariable } from '../../types'; interface Props { messageVariables?: ActionVariable[]; paramsProperty: string; - inputTargetValue: string; + inputTargetValue?: string; label: string; errors?: string[]; areaLabel?: string; onDocumentsChange: (data: string) => void; helpText?: JSX.Element; + onBlur?: () => void; } export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ @@ -31,6 +32,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ areaLabel, onDocumentsChange, helpText, + onBlur, }) => { const [cursorPosition, setCursorPosition] = useState(null); @@ -84,6 +86,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ onDocumentsChange(convertToJson(xjson)); }} onCursorChange={(_value: any) => onClickWithMessageVariable(_value)} + onBlur={onBlur} /> ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts index 5f2e9d07c4bcb..9fb3a54d5a182 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts @@ -10,8 +10,6 @@ import { HelloWorldEmbeddable } from '../../../../examples/embeddable_examples/p /** eslint-enable */ import { TimeRangeEmbeddable, TimeRangeContainer } from './test_helpers'; -jest.mock('ui/new_platform'); - test('canInheritTimeRange returns false if embeddable is inside container without a time range', () => { const embeddable = new TimeRangeEmbeddable( { id: '1234', timeRange: { from: 'noxw-15m', to: 'now' } }, 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/custom_time_range_action.test.ts b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts index 7919d2d2482ae..9af79c9ac5259 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts @@ -21,8 +21,6 @@ import { import { nextTick } from 'test_utils/enzyme_helpers'; import { ReactElement } from 'react'; -jest.mock('ui/new_platform'); - const createOpenModalMock = () => { const mock = jest.fn(); mock.mockReturnValue({ close: jest.fn() }); 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/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx new file mode 100644 index 0000000000000..244ea9bd2a97e --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx @@ -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. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { Demo } from './test_samples/demo'; + +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/public/breadcrumbs.ts b/x-pack/plugins/uptime/public/breadcrumbs.ts deleted file mode 100644 index 41bc2aa258807..0000000000000 --- a/x-pack/plugins/uptime/public/breadcrumbs.ts +++ /dev/null @@ -1,5 +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. - */ diff --git a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap index cf80d2de38b3f..261f3a016d4db 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap @@ -44,18 +44,23 @@ exports[`DonutChart component passes correct props without errors for valid prop }, }, "axes": Object { - "axisLineStyle": Object { + "axisLine": Object { "stroke": "#eaeaea", "strokeWidth": 1, + "visible": true, }, - "axisTitleStyle": Object { + "axisTitle": Object { "fill": "#333", "fontFamily": "sans-serif", "fontSize": 12, "fontStyle": "bold", - "padding": 8, + "padding": Object { + "inner": 8, + "outer": 0, + }, + "visible": true, }, - "gridLineStyle": Object { + "gridLine": Object { "horizontal": Object { "dash": Array [ 0, @@ -64,7 +69,7 @@ exports[`DonutChart component passes correct props without errors for valid prop "opacity": 1, "stroke": "#D3DAE6", "strokeWidth": 1, - "visible": true, + "visible": false, }, "vertical": Object { "dash": Array [ @@ -74,17 +79,30 @@ exports[`DonutChart component passes correct props without errors for valid prop "opacity": 1, "stroke": "#D3DAE6", "strokeWidth": 1, - "visible": true, + "visible": false, }, }, - "tickLabelStyle": Object { + "tickLabel": Object { + "alignment": Object { + "horizontal": "near", + "vertical": "near", + }, "fill": "#777", "fontFamily": "sans-serif", "fontSize": 10, "fontStyle": "normal", - "padding": 4, + "offset": Object { + "reference": "local", + "x": 0, + "y": 0, + }, + "padding": 0, + "rotation": 0, + "visible": true, }, - "tickLineStyle": Object { + "tickLine": Object { + "padding": 10, + "size": 10, "stroke": "#eaeaea", "strokeWidth": 1, "visible": true, @@ -164,6 +182,7 @@ exports[`DonutChart component passes correct props without errors for valid prop }, "legend": Object { "horizontalHeight": 64, + "margin": 0, "spacingBuffer": 10, "verticalWidth": 200, }, @@ -217,17 +236,20 @@ exports[`DonutChart component passes correct props without errors for valid prop }, }, "axes": Object { - "axisLineStyle": Object { + "axisLine": Object { "stroke": "rgba(238, 240, 243, 1)", }, - "axisTitleStyle": Object { + "axisTitle": Object { "fill": "rgba(52, 55, 65, 1)", "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", "fontSize": 12, - "padding": 10, + "padding": Object { + "inner": 10, + "outer": 0, + }, }, - "gridLineStyle": Object { + "gridLine": Object { "horizontal": Object { "dash": Array [ 0, @@ -249,14 +271,17 @@ exports[`DonutChart component passes correct props without errors for valid prop "visible": true, }, }, - "tickLabelStyle": Object { + "tickLabel": Object { "fill": "rgba(105, 112, 125, 1)", "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", "fontSize": 10, - "padding": 8, + "padding": Object { + "inner": 10, + "outer": 8, + }, }, - "tickLineStyle": Object { + "tickLine": Object { "stroke": "rgba(238, 240, 243, 1)", "strokeWidth": 1, "visible": false, 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/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts index f0222de02697d..015d9a4925f3e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts @@ -203,28 +203,32 @@ describe('monitor availability', () => { }, }, }, - ], - "minimum_should_match": 1, - "should": Array [ Object { "bool": Object { "minimum_should_match": 1, "should": Array [ Object { - "match_phrase": Object { - "monitor.id": "apm-dev", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "apm-dev", + }, + }, + ], }, }, - ], - }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ Object { - "match_phrase": Object { - "monitor.id": "auto-http-0X8D6082B94BBE3B8A", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "auto-http-0X8D6082B94BBE3B8A", + }, + }, + ], }, }, ], @@ -797,6 +801,133 @@ describe('monitor availability', () => { ] `); }); + + it('does not overwrite filters', async () => { + const [callES, esMock] = setupMockEsCompositeQuery< + AvailabilityKey, + GetMonitorAvailabilityResult, + AvailabilityDoc + >( + [ + { + bucketCriteria: [], + }, + ], + genBucketItem + ); + await getMonitorAvailability({ + callES, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + range: 3, + rangeUnit: 's', + threshold: '99', + filters: JSON.stringify({ bool: { filter: [{ term: { 'monitor.id': 'foo' } }] } }), + }); + const [, params] = esMock.callAsCurrentUser.mock.calls[0]; + expect(params).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "down_sum": Object { + "sum": Object { + "field": "summary.down", + "missing": 0, + }, + }, + "fields": Object { + "top_hits": Object { + "size": 1, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "filtered": Object { + "bucket_selector": Object { + "buckets_path": Object { + "threshold": "ratio.value", + }, + "script": "params.threshold < 0.99", + }, + }, + "ratio": Object { + "bucket_script": Object { + "buckets_path": Object { + "downTotal": "down_sum", + "upTotal": "up_sum", + }, + "script": " + if (params.upTotal + params.downTotal > 0) { + return params.upTotal / (params.upTotal + params.downTotal); + } return null;", + }, + }, + "up_sum": Object { + "sum": Object { + "field": "summary.up", + "missing": 0, + }, + }, + }, + "composite": Object { + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-3s", + "lte": "now", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "monitor.id": "foo", + }, + }, + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + } + `); + }); }); describe('formatBuckets', () => { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index 7dba71a8126e2..e61d736e37106 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -148,28 +148,32 @@ describe('getMonitorStatus', () => { }, }, }, - ], - "minimum_should_match": 1, - "should": Array [ Object { "bool": Object { "minimum_should_match": 1, "should": Array [ Object { - "match_phrase": Object { - "monitor.id": "apm-dev", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "apm-dev", + }, + }, + ], }, }, - ], - }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ Object { - "match_phrase": Object { - "monitor.id": "auto-http-0X8D6082B94BBE3B8A", + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "auto-http-0X8D6082B94BBE3B8A", + }, + }, + ], }, }, ], @@ -286,6 +290,296 @@ describe('getMonitorStatus', () => { `); }); + it('properly assigns filters for complex kuery filters', async () => { + const [callES, esMock] = setupMockEsCompositeQuery( + [{ bucketCriteria: [] }], + genBucketItem + ); + const clientParameters = { + timerange: { + from: 'now-15m', + to: 'now', + }, + numTimes: 5, + locations: [], + filters: { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + tags: 'org:google', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { + match_phrase: { + 'monitor.type': 'http', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'monitor.type': 'tcp', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + }; + await getMonitorStatus({ + callES, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + ...clientParameters, + }); + expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); + const [, params] = esMock.callAsCurrentUser.mock.calls[0]; + expect(params).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "fields": Object { + "top_hits": Object { + "size": 1, + }, + }, + }, + "composite": Object { + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "status": Object { + "terms": Object { + "field": "monitor.status", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "monitor.status": "down", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-15m", + "lte": "now", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "tags": "org:google", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.type": "http", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.type": "tcp", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + } + `); + }); + + it('properly assigns filters for complex kuery filters object', async () => { + const [callES, esMock] = setupMockEsCompositeQuery( + [{ bucketCriteria: [] }], + genBucketItem + ); + const clientParameters = { + timerange: { + from: 'now-15m', + to: 'now', + }, + numTimes: 5, + locations: [], + filters: { + bool: { + filter: { + exists: { + field: 'monitor.status', + }, + }, + }, + }, + }; + await getMonitorStatus({ + callES, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + ...clientParameters, + }); + expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); + const [, params] = esMock.callAsCurrentUser.mock.calls[0]; + expect(params).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "fields": Object { + "top_hits": Object { + "size": 1, + }, + }, + }, + "composite": Object { + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "status": Object { + "terms": Object { + "field": "monitor.status", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "monitor.status": "down", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-15m", + "lte": "now", + }, + }, + }, + Object { + "bool": Object { + "filter": Object { + "exists": Object { + "field": "monitor.status", + }, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + } + `); + }); + it('fetches single page of results', async () => { const [callES, esMock] = setupMockEsCompositeQuery( [ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts index 0801fc5769363..f78048100b998 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts @@ -47,6 +47,11 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< const gte = `now-${range}${rangeUnit}`; + let parsedFilters: any; + if (filters) { + parsedFilters = JSON.parse(filters); + } + do { const esParams: any = { index: dynamicSettings.heartbeatIndices, @@ -62,6 +67,8 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< }, }, }, + // append user filters, if defined + ...(parsedFilters?.bool ? [parsedFilters] : []), ], }, }, @@ -139,11 +146,6 @@ export const getMonitorAvailability: UMElasticsearchQueryFn< }, }; - if (filters) { - const parsedFilter = JSON.parse(filters); - esParams.body.query.bool = { ...esParams.body.query.bool, ...parsedFilter.bool }; - } - if (afterKey) { esParams.body.aggs.monitors.composite.after = afterKey; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts index beed8e8335314..caf505610e991 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts @@ -71,6 +71,8 @@ export const getMonitorStatus: UMElasticsearchQueryFn< }, }, }, + // append user filters, if defined + ...(filters?.bool ? [filters] : []), ], }, }, @@ -116,10 +118,6 @@ export const getMonitorStatus: UMElasticsearchQueryFn< }, }; - if (filters?.bool) { - esParams.body.query.bool = Object.assign({}, esParams.body.query.bool, filters.bool); - } - /** * Perform a logical `and` against the selected location filters. */ diff --git a/x-pack/plugins/watcher/public/application/lib/api.ts b/x-pack/plugins/watcher/public/application/lib/api.ts index 82ec2925ba6dc..4f42b26d51c46 100644 --- a/x-pack/plugins/watcher/public/application/lib/api.ts +++ b/x-pack/plugins/watcher/public/application/lib/api.ts @@ -35,44 +35,53 @@ export const getSavedObjectsClient = () => savedObjectsClient; const basePath = ROUTES.API_ROOT; +const loadWatchesDeserializer = ({ watches = [] }: { watches: any[] }) => { + return watches.map((watch: any) => Watch.fromUpstreamJson(watch)); +}; + export const useLoadWatches = (pollIntervalMs: number) => { return useRequest({ path: `${basePath}/watches`, method: 'get', pollIntervalMs, - deserializer: ({ watches = [] }: { watches: any[] }) => { - return watches.map((watch: any) => Watch.fromUpstreamJson(watch)); - }, + deserializer: loadWatchesDeserializer, }); }; +const loadWatchDetailDeserializer = ({ watch = {} }: { watch: any }) => + Watch.fromUpstreamJson(watch); + export const useLoadWatchDetail = (id: string) => { return useRequest({ path: `${basePath}/watch/${id}`, method: 'get', - deserializer: ({ watch = {} }: { watch: any }) => Watch.fromUpstreamJson(watch), + deserializer: loadWatchDetailDeserializer, }); }; +const loadWatchHistoryDeserializer = ({ watchHistoryItems = [] }: { watchHistoryItems: any }) => { + return watchHistoryItems.map((historyItem: any) => + WatchHistoryItem.fromUpstreamJson(historyItem) + ); +}; + export const useLoadWatchHistory = (id: string, startTime: string) => { return useRequest({ query: startTime ? { startTime } : undefined, path: `${basePath}/watch/${id}/history`, method: 'get', - deserializer: ({ watchHistoryItems = [] }: { watchHistoryItems: any }) => { - return watchHistoryItems.map((historyItem: any) => - WatchHistoryItem.fromUpstreamJson(historyItem) - ); - }, + deserializer: loadWatchHistoryDeserializer, }); }; +const loadWatchHistoryDetailDeserializer = ({ watchHistoryItem }: { watchHistoryItem: any }) => + WatchHistoryItem.fromUpstreamJson(watchHistoryItem); + export const useLoadWatchHistoryDetail = (id: string | undefined) => { return useRequest({ path: !id ? '' : `${basePath}/history/${id}`, method: 'get', - deserializer: ({ watchHistoryItem }: { watchHistoryItem: any }) => - WatchHistoryItem.fromUpstreamJson(watchHistoryItem), + deserializer: loadWatchHistoryDetailDeserializer, }); }; @@ -148,6 +157,8 @@ export const loadIndexPatterns = async () => { return savedObjects; }; +const getWatchVisualizationDataDeserializer = (data: { visualizeData: any }) => data?.visualizeData; + export const useGetWatchVisualizationData = (watchModel: BaseWatch, visualizeOptions: any) => { return useRequest({ path: `${basePath}/watch/visualize`, @@ -156,21 +167,23 @@ export const useGetWatchVisualizationData = (watchModel: BaseWatch, visualizeOpt watch: watchModel.upstreamJson, options: visualizeOptions.upstreamJson, }), - deserializer: (data: { visualizeData: any }) => data?.visualizeData, + deserializer: getWatchVisualizationDataDeserializer, }); }; +const loadSettingsDeserializer = (data: { + action_types: { + [key: string]: { + enabled: boolean; + }; + }; +}) => Settings.fromUpstreamJson(data); + export const useLoadSettings = () => { return useRequest({ path: `${basePath}/settings`, method: 'get', - deserializer: (data: { - action_types: { - [key: string]: { - enabled: boolean; - }; - }; - }) => Settings.fromUpstreamJson(data), + deserializer: loadSettingsDeserializer, }); }; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx index c0d906114277e..2ff0f53d07e91 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useContext, useEffect } from 'react'; +import React, { Fragment, useContext, useEffect, useMemo } from 'react'; import { AnnotationDomainTypes, Axis, @@ -105,7 +105,9 @@ export const WatchVisualization = () => { threshold, } = watch; - const domain = getDomain(watch); + // Only recalculate the domain if the watch configuration changes. This prevents the visualization + // request's resolution from re-triggering itself in an infinite loop. + const domain = useMemo(() => getDomain(watch), [watch]); const timeBuckets = createTimeBuckets(); timeBuckets.setBounds(domain); const interval = timeBuckets.getInterval().expression; diff --git a/x-pack/plugins/watcher/public/application/shared_imports.ts b/x-pack/plugins/watcher/public/application/shared_imports.ts index a9e07b80a9b22..766e8e659c8ae 100644 --- a/x-pack/plugins/watcher/public/application/shared_imports.ts +++ b/x-pack/plugins/watcher/public/application/shared_imports.ts @@ -10,6 +10,6 @@ export { UseRequestConfig, sendRequest, useRequest, -} from '../../../../../src/plugins/es_ui_shared/public/'; +} from '../../../../../src/plugins/es_ui_shared/public'; export { useXJsonMode } from '../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 421e709444d0f..6271c4b601307 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -56,7 +56,6 @@ const onlyNotInCoverageTests = [ require.resolve('../test/upgrade_assistant_integration/config.js'), require.resolve('../test/licensing_plugin/config.ts'), require.resolve('../test/licensing_plugin/config.public.ts'), - require.resolve('../test/licensing_plugin/config.legacy.ts'), require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), require.resolve('../test/reporting_api_integration/reporting_and_security.config.ts'), require.resolve('../test/reporting_api_integration/reporting_without_security.config.ts'), diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 67dd8c877e378..f9fdfaed1c79b 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -63,7 +63,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) const actionsProxyUrl = options.enableActionsProxy ? [ `--xpack.actions.proxyUrl=http://localhost:${proxyPort}`, - '--xpack.actions.rejectUnauthorizedCertificates=false', + '--xpack.actions.proxyRejectUnauthorizedCertificates=false', ] : []; 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/logstash/pipeline/delete.ts b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts index 2a8cde85ee3cd..e3d68ef690359 100644 --- a/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts +++ b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts @@ -18,7 +18,6 @@ export default function ({ getService }: FtrProviderContext) { .put('/api/logstash/pipeline/fast_generator') .set('kbn-xsrf', 'xxx') .send({ - id: 'fast_generator', description: 'foobar baz', pipeline: 'input { generator {} }\n\n output { stdout {} }', }) diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/save.ts b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts index f44c5e6252d5f..1c2f23e81eafc 100644 --- a/x-pack/test/api_integration/apis/logstash/pipeline/save.ts +++ b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts @@ -26,7 +26,6 @@ export default function ({ getService }: FtrProviderContext) { .put('/api/logstash/pipeline/fast_generator') .set('kbn-xsrf', 'xxx') .send({ - id: 'fast_generator', description: 'foobar baz', pipeline: 'input { generator {} }\n\n output { stdout {} }', }) diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js new file mode 100644 index 0000000000000..7219fc858e059 --- /dev/null +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -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. + */ + +export default function ({ getService }) { + const supertest = getService('supertest'); + + describe('getTile', () => { + it('should validate params', async () => { + await supertest + .get( + `/api/maps/mvt/getTile?x=15&y=11&z=5&geometryFieldName=coordinates&index=logstash*&requestBody=(_source:(includes:!(coordinates)),docvalue_fields:!(),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(coordinates))` + ) + .set('kbn-xsrf', 'kibana') + .expect(200); + }); + + it('should not validate when required params are missing', async () => { + await supertest + .get( + `/api/maps/mvt/getTile?&index=logstash*&requestBody=(_source:(includes:!(coordinates)),docvalue_fields:!(),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(coordinates))` + ) + .set('kbn-xsrf', 'kibana') + .expect(400); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/maps/index.js b/x-pack/test/api_integration/apis/maps/index.js index f9dff19229645..6c213380dd64e 100644 --- a/x-pack/test/api_integration/apis/maps/index.js +++ b/x-pack/test/api_integration/apis/maps/index.js @@ -16,6 +16,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./fonts_api')); loadTestFile(require.resolve('./index_settings')); loadTestFile(require.resolve('./migrations')); + loadTestFile(require.resolve('./get_tile')); }); }); } 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/apm_api_integration/basic/tests/transaction_groups/breakdown.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts index 5b61112a374c1..0b94abaa15890 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import expectedBreakdown from './expectation/breakdown.json'; -import expectedBreakdownWithTransactionName from './expectation/breakdown_transaction_name.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -25,7 +24,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` ); expect(response.status).to.be(200); - expect(response.body).to.eql({ kpis: [], timeseries: [] }); + expect(response.body).to.eql({ timeseries: [] }); }); }); @@ -47,15 +46,32 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql(expectedBreakdownWithTransactionName); + const { timeseries } = response.body; + const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; + expect(data).to.eql([ + { x: 1593413100000, y: null }, + { x: 1593413130000, y: null }, + { x: 1593413160000, y: null }, + { x: 1593413190000, y: null }, + { x: 1593413220000, y: null }, + { x: 1593413250000, y: null }, + { x: 1593413280000, y: null }, + { x: 1593413310000, y: 1 }, + { x: 1593413340000, y: null }, + ]); + expect(title).to.be('app'); + expect(color).to.be('#54b399'); + expect(type).to.be('areaStacked'); + expect(hideLegend).to.be(false); + expect(legendValue).to.be('100%'); }); - it('returns the top 4 by percentage and sorts them by name', async () => { + it('returns the transaction breakdown sorted by name', async () => { const response = await supertest.get( `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` ); expect(response.status).to.be(200); - expect(response.body.kpis.map((kpi: { name: string }) => kpi.name)).to.eql([ + expect(response.body.timeseries.map((serie: { title: string }) => serie.title)).to.eql([ 'app', 'http', 'postgresql', diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json index 3b884a9eb7907..8ffbba64ec7ab 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json @@ -1,26 +1,4 @@ { - "kpis":[ - { - "name":"app", - "percentage":0.16700861715223636, - "color":"#54b399" - }, - { - "name":"http", - "percentage":0.7702092736971686, - "color":"#6092c0" - }, - { - "name":"postgresql", - "percentage":0.0508822322527698, - "color":"#d36086" - }, - { - "name":"redis", - "percentage":0.011899876897825195, - "color":"#9170b8" - } - ], "timeseries":[ { "title":"app", @@ -64,7 +42,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "17%" }, { "title":"http", @@ -108,7 +87,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "77%" }, { "title":"postgresql", @@ -152,7 +132,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "5.1%" }, { "title":"redis", @@ -196,7 +177,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "1.2%" } ] } \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json deleted file mode 100644 index b4f8e376d3609..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "kpis":[ - { - "name":"app", - "percentage":1, - "color":"#54b399" - } - ], - "timeseries":[ - { - "title":"app", - "color":"#54b399", - "type":"areaStacked", - "data":[ - { - "x":1593413100000, - "y":null - }, - { - "x":1593413130000, - "y":null - }, - { - "x":1593413160000, - "y":null - }, - { - "x":1593413190000, - "y":null - }, - { - "x":1593413220000, - "y":null - }, - { - "x":1593413250000, - "y":null - }, - { - "x":1593413280000, - "y":null - }, - { - "x":1593413310000, - "y":1 - }, - { - "x":1593413340000, - "y":null - } - ], - "hideLegend":true - } - ] -} \ No newline at end of file diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts index a022b7c79c079..c682c1f1f4640 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts @@ -49,7 +49,7 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllTimelines(es); }); - it('should contain two output keys of rules_installed and rules_updated', async () => { + it('should contain rules_installed, rules_updated, timelines_installed, and timelines_updated', async () => { const { body } = await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) .set('kbn-xsrf', 'true') @@ -74,6 +74,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_installed).to.be.greaterThan(0); }); + it('should create the prepackaged timelines and return a count greater than zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.be.greaterThan(0); + }); + it('should create the prepackaged rules that the rules_updated is of size zero', async () => { const { body } = await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) @@ -84,6 +94,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_updated).to.eql(0); }); + it('should create the prepackaged timelines and the timelines_updated is of size zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_updated).to.eql(0); + }); + it('should be possible to call the API twice and the second time the number of rules installed should be zero', async () => { await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) @@ -109,6 +129,30 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_installed).to.eql(0); }); + + it('should be possible to call the API twice and the second time the number of timelines installed should be zero', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + await waitFor(async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .expect(200); + return body.timelines_not_installed === 0; + }); + + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.eql(0); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts new file mode 100644 index 0000000000000..209c7974a7c2a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts @@ -0,0 +1,403 @@ +/* + * 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 { TIMELINE_IMPORT_URL } from '../../../../plugins/security_solution/common/constants'; + +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { deleteAllTimelines } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('import timelines', () => { + describe('creating a timeline', () => { + const getTimeline = () => { + return Buffer.from( + JSON.stringify({ + savedObjectId: '67664480-d191-11ea-ae67-4f4be8c1847b', + version: 'WzU1NSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'default', + kqlQuery: { + filterQuery: { + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"@timestamp"}}],"minimum_should_match":1}}', + kuery: { + expression: '@timestamp : * ', + kind: 'kuery', + }, + }, + }, + title: 'x2', + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + created: 1596036895488, + createdBy: 'angela', + updated: 1596491470411, + updatedBy: 'elastic', + templateTimelineId: null, + templateTimelineVersion: null, + dateRange: { + start: '2020-04-10T14:10:58.373Z', + end: '2020-05-30T14:16:58.373Z', + }, + savedQueryId: null, + eventNotes: [ + { + noteId: '7d875ba0-d5d3-11ea-9899-ebec3d084fe0', + version: 'WzU1NiwxXQ==', + eventId: '8KtMKnIBOS_moQ_K9fAe', + note: 'hi Xavier', + timelineId: '67664480-d191-11ea-ae67-4f4be8c1847b', + created: 1596491490806, + createdBy: 'elastic', + updated: 1596491490806, + updatedBy: 'elastic', + }, + ], + globalNotes: [], + pinnedEventIds: [ + 'K99zy3EBDTDlbwBfpf6x', + 'GKpFKnIBOS_moQ_Ke5AO', + '8KtMKnIBOS_moQ_K9fAe', + ], + }) + ); + }; + beforeEach(async () => {}); + + afterEach(async () => { + await deleteAllTimelines(es); + }); + + it("if it doesn't exists", async () => { + const { body } = await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimeline(), 'timelines.ndjson') + .expect(200); + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 1, + timelines_updated: 0, + }); + }); + }); + + describe('creating a timeline template', () => { + const getTimelineTemplate = () => { + return Buffer.from( + JSON.stringify({ + savedObjectId: 'cab434d0-d26c-11ea-b887-3b103296472a', + version: 'WzQ0NSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: 'desc', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'template', + kqlQuery: { filterQuery: null }, + title: 'my t template', + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + templateTimelineId: '46a50505-0a48-49cb-9ab2-d15d683efa3b', + templateTimelineVersion: 1, + created: 1596473742379, + createdBy: 'elastic', + updated: 1596473909169, + updatedBy: 'elastic', + dateRange: { start: '2020-08-02T16:55:22.160Z', end: '2020-08-03T16:55:22.161Z' }, + savedQueryId: null, + eventNotes: [], + globalNotes: [ + { + noteId: '358f45c0-d5aa-11ea-9b6d-53d136d390dc', + version: 'WzQzOCwxXQ==', + note: 'I have a global note', + timelineId: 'cab434d0-d26c-11ea-b887-3b103296472a', + created: 1596473760688, + createdBy: 'elastic', + updated: 1596473760688, + updatedBy: 'elastic', + }, + ], + pinnedEventIds: [], + }) + ); + }; + + afterEach(async () => { + await deleteAllTimelines(es); + }); + + it("if it doesn't exists", async () => { + const { body } = await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimelineTemplate(), 'timelines.ndjson') + .expect(200); + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 1, + timelines_updated: 0, + }); + }); + }); + + describe('Updating a timeline template', () => { + const getTimelineTemplate = (templateTimelineVersion: number) => { + return Buffer.from( + JSON.stringify({ + savedObjectId: 'cab434d0-d26c-11ea-b887-3b103296472a', + version: 'WzQ0NSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: 'desc', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'template', + kqlQuery: { filterQuery: null }, + title: 'my t template', + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + templateTimelineId: '46a50505-0a48-49cb-9ab2-d15d683efa3b', + templateTimelineVersion, + created: 1596473742379, + createdBy: 'elastic', + updated: 1596473909169, + updatedBy: 'elastic', + dateRange: { start: '2020-08-02T16:55:22.160Z', end: '2020-08-03T16:55:22.161Z' }, + savedQueryId: null, + eventNotes: [], + globalNotes: [ + { + noteId: '358f45c0-d5aa-11ea-9b6d-53d136d390dc', + version: 'WzQzOCwxXQ==', + note: 'I have a global note', + timelineId: 'cab434d0-d26c-11ea-b887-3b103296472a', + created: 1596473760688, + createdBy: 'elastic', + updated: 1596473760688, + updatedBy: 'elastic', + }, + ], + pinnedEventIds: [], + }) + ); + }; + + afterEach(async () => { + await deleteAllTimelines(es); + }); + + it("if it doesn't exists", async () => { + await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimelineTemplate(1), 'timelines.ndjson') + .expect(200); + const { body } = await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimelineTemplate(2), 'timelines.ndjson') + .expect(200); + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 0, + timelines_updated: 1, + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts index a480e63ff4a92..09feb7ac5e0ec 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts @@ -28,5 +28,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./patch_rules')); loadTestFile(require.resolve('./query_signals')); loadTestFile(require.resolve('./open_close_signals')); + loadTestFile(require.resolve('./import_timelines')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts b/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts new file mode 100644 index 0000000000000..556217877968b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.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 expect from '@kbn/expect'; + +import { TIMELINE_PREPACKAGED_URL } from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteAllTimelines, + deleteSignalsIndex, + waitFor, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('install_prepackaged_timelines', () => { + describe('creating prepackaged rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllTimelines(es); + }); + + it('should contain timelines_installed, and timelines_updated', async () => { + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(Object.keys(body)).to.eql(['timelines_installed', 'timelines_updated']); + }); + + it('should create the prepackaged timelines and return a count greater than zero', async () => { + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.be.greaterThan(0); + }); + + it('should create the prepackaged timelines and the timelines_updated is of size zero', async () => { + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_updated).to.eql(0); + }); + + it('should be possible to call the API twice and the second time the number of timelines installed should be zero', async () => { + await supertest.put(TIMELINE_PREPACKAGED_URL).set('kbn-xsrf', 'true').send().expect(200); + + await waitFor(async () => { + const { body } = await supertest + .get(`${TIMELINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .expect(200); + return body.timelines_not_installed === 0; + }); + + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.eql(0); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts deleted file mode 100644 index 29ead0db1c634..0000000000000 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -const DRILLDOWN_TO_PIE_CHART_NAME = 'Go to pie chart dashboard'; -const DRILLDOWN_TO_AREA_CHART_NAME = 'Go to area chart dashboard'; - -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']); - const pieChart = getService('pieChart'); - const log = getService('log'); - const browser = getService('browser'); - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - const filterBar = getService('filterBar'); - - describe('Dashboard Drilldowns', function () { - before(async () => { - log.debug('Dashboard Drilldowns:initTests'); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - }); - - it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { - await PageObjects.dashboard.gotoDashboardEditMode( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME - ); - - // create drilldown - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); - await dashboardDrilldownPanelActions.clickCreateDrilldown(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); - await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ - drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, - destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, - }); - await dashboardDrilldownsManage.saveChanges(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); - - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); - - // save dashboard, navigate to view mode - await PageObjects.dashboard.saveDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, - { - saveAsNew: false, - waitDialogIsClosed: true, - } - ); - - // trigger drilldown action by clicking on a pie and picking drilldown action by it's name - await pieChart.clickOnPieSlice('40,000'); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - const href = await dashboardDrilldownPanelActions.getActionHrefByText( - DRILLDOWN_TO_AREA_CHART_NAME - ); - expect(typeof href).to.be('string'); // checking that action has a href - const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); - }); - // checking that href is at least pointing to the same dashboard that we are navigated to by regular click - expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl()); - - // check that we drilled-down with filter from pie chart - expect(await filterBar.getFilterCount()).to.be(1); - - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - - // brush area chart and drilldown back to pie chat dashboard - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); - }); - - // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) - expect(await filterBar.getFilterCount()).to.be(1); - await pieChart.expectPieSliceCount(1); - - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - - // delete drilldown - await PageObjects.dashboard.switchToEditMode(); - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); - await dashboardDrilldownPanelActions.clickManageDrilldowns(); - await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); - - await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); - await dashboardDrilldownsManage.closeFlyout(); - - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); - }); - - it('browser back/forward navigation works after drilldown navigation', async () => { - await PageObjects.dashboard.loadSavedDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME - ); - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); - }); - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - - await navigateWithinDashboard(async () => { - await browser.goBack(); - }); - - expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( - 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, - }, - } - ); - } - - async function navigateWithinDashboard(navigationTrigger: () => Promise) { - // before executing action which would trigger navigation: remember current dashboard id in url - const oldDashboardId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); - // execute navigation action - await navigationTrigger(); - // wait until dashboard navigates to a new dashboard with area chart - await retry.waitFor('navigate to different dashboard', async () => { - const newDashboardId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); - return typeof newDashboardId === 'string' && oldDashboardId !== newDashboardId; - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.waitForRenderComplete(); - } -} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts new file mode 100644 index 0000000000000..c300412c393bc --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -0,0 +1,171 @@ +/* + * 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_PIE_CHART_NAME = 'Go to pie chart dashboard'; +const DRILLDOWN_TO_AREA_CHART_NAME = 'Go to area chart dashboard'; + +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']); + const pieChart = getService('pieChart'); + const log = getService('log'); + const browser = getService('browser'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const filterBar = getService('filterBar'); + + describe('Dashboard to dashboard drilldown', function () { + before(async () => { + log.debug('Dashboard Drilldowns:initTests'); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + }); + + it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { + await PageObjects.dashboard.gotoDashboardEditMode( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME + ); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ + drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, + destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, + { + saveAsNew: false, + waitDialogIsClosed: true, + } + ); + + // trigger drilldown action by clicking on a pie and picking drilldown action by it's name + await pieChart.clickOnPieSlice('40,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + const href = await dashboardDrilldownPanelActions.getActionHrefByText( + DRILLDOWN_TO_AREA_CHART_NAME + ); + expect(typeof href).to.be('string'); // checking that action has a href + const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); + }); + // checking that href is at least pointing to the same dashboard that we are navigated to by regular click + expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl()); + + // check that we drilled-down with filter from pie chart + expect(await filterBar.getFilterCount()).to.be(1); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + + // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) + expect(await filterBar.getFilterCount()).to.be(1); + await pieChart.expectPieSliceCount(1); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + // delete drilldown + await PageObjects.dashboard.switchToEditMode(); + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); + await dashboardDrilldownPanelActions.clickManageDrilldowns(); + await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); + + await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); + await dashboardDrilldownsManage.closeFlyout(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + }); + + it('browser back/forward navigation works after drilldown navigation', async () => { + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + await navigateWithinDashboard(async () => { + await browser.goBack(); + }); + + expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( + 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, + }, + } + ); + } + + async function navigateWithinDashboard(navigationTrigger: () => Promise) { + // before executing action which would trigger navigation: remember current dashboard id in url + const oldDashboardId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + // execute navigation action + await navigationTrigger(); + // wait until dashboard navigates to a new dashboard with area chart + await retry.waitFor('navigate to different dashboard', async () => { + const newDashboardId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + return typeof newDashboardId === 'string' && oldDashboardId !== newDashboardId; + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + } +} 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/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 04f289b69bb71..82aba0503fb9d 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -21,7 +21,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'infraHome']); const supertest = getService('supertest'); - describe('Home page', function () { + // FLAKY: https://github.com/elastic/kibana/issues/75724 + describe.skip('Home page', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('empty_kibana'); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 4bbe38367d0a2..ef8b4ad4c0f19 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -46,6 +46,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./es_geo_grid_source')); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); + loadTestFile(require.resolve('./mvt_scaling')); loadTestFile(require.resolve('./add_layer_panel')); loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index e447996a08dfe..1139ae204aefd 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { set } from '@elastic/safer-lodash-set'; import { MAPBOX_STYLES } from './mapbox_styles'; @@ -21,6 +20,7 @@ const VECTOR_SOURCE_ID = 'n1t6f'; const CIRCLE_STYLE_LAYER_INDEX = 0; const FILL_STYLE_LAYER_INDEX = 2; const LINE_STYLE_LAYER_INDEX = 3; +const TOO_MANY_FEATURES_LAYER_INDEX = 4; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); @@ -87,28 +87,32 @@ export default function ({ getPageObjects, getService }) { }); }); - it('should style fills, points and lines independently', async () => { + it('should style fills, points, lines, and bounding-boxes independently', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); const layersForVectorSource = mapboxStyle.layers.filter((mbLayer) => { return mbLayer.id.startsWith(VECTOR_SOURCE_ID); }); - // Color is dynamically obtained from eui source lib - const dynamicColor = - layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX].paint['circle-stroke-color']; - //circle layer for points - expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql( - set(MAPBOX_STYLES.POINT_LAYER, 'paint.circle-stroke-color', dynamicColor) - ); + expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.POINT_LAYER); //fill layer expect(layersForVectorSource[FILL_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.FILL_LAYER); //line layer for borders - expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql( - set(MAPBOX_STYLES.LINE_LAYER, 'paint.line-color', dynamicColor) - ); + expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.LINE_LAYER); + + //Too many features layer (this is a static style config) + expect(layersForVectorSource[TOO_MANY_FEATURES_LAYER_INDEX]).to.eql({ + id: 'n1t6f_toomanyfeatures', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: ['==', ['get', '__kbn_too_many_features__'], true], + layout: { visibility: 'visible' }, + paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, + }); }); it('should flag only the joined features as visible', async () => { diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 744eb4ac74bf6..78720fa1689ec 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -14,7 +14,11 @@ export const MAPBOX_STYLES = { filter: [ 'all', ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], + [ + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], + ], ], layout: { visibility: 'visible' }, paint: { @@ -84,7 +88,11 @@ export const MAPBOX_STYLES = { filter: [ 'all', ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], + [ + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], + ], ], layout: { visibility: 'visible' }, paint: { @@ -151,20 +159,18 @@ export const MAPBOX_STYLES = { 'all', ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'any', - ['==', ['geometry-type'], 'Polygon'], - ['==', ['geometry-type'], 'MultiPolygon'], - ['==', ['geometry-type'], 'LineString'], - ['==', ['geometry-type'], 'MultiLineString'], + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + [ + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], + ['==', ['geometry-type'], 'LineString'], + ['==', ['geometry-type'], 'MultiLineString'], + ], ], ], - layout: { - visibility: 'visible', - }, - paint: { - /* 'line-color': '' */ // Obtained dynamically - 'line-opacity': 0.75, - 'line-width': 1, - }, + layout: { visibility: 'visible' }, + paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, }, }; diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/mvt_scaling.js new file mode 100644 index 0000000000000..e50b72658fb43 --- /dev/null +++ b/x-pack/test/functional/apps/maps/mvt_scaling.js @@ -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 expect from '@kbn/expect'; + +const VECTOR_SOURCE_ID = 'caffa63a-ebfb-466d-8ff6-d797975b88ab'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + + describe('mvt geoshape layer', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('geo_shape_mvt'); + }); + + after(async () => { + await inspector.close(); + }); + + it('should render with mvt-source', async () => { + const mapboxStyle = await PageObjects.maps.getMapboxStyle(); + + //Source should be correct + expect(mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0]).to.equal( + '/api/maps/mvt/getTile?x={x}&y={y}&z={z}&geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:(includes:!(geometry,prop1)),docvalue_fields:!(prop1),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(geometry,prop1))' + ); + + //Should correctly load meta for style-rule (sigma is set to 1, opacity to 1) + const fillLayer = mapboxStyle.layers.find((layer) => layer.id === VECTOR_SOURCE_ID + '_fill'); + expect(fillLayer.paint).to.eql({ + 'fill-color': [ + 'interpolate', + ['linear'], + [ + 'coalesce', + [ + 'case', + ['==', ['get', 'prop1'], null], + 0.3819660112501051, + [ + 'max', + ['min', ['to-number', ['get', 'prop1']], 3.618033988749895], + 1.381966011250105, + ], + ], + 0.3819660112501051, + ], + 0.3819660112501051, + 'rgba(0,0,0,0)', + 1.381966011250105, + '#ecf1f7', + 1.6614745084375788, + '#d9e3ef', + 1.9409830056250525, + '#c5d5e7', + 2.2204915028125263, + '#b2c7df', + 2.5, + '#9eb9d8', + 2.7795084971874737, + '#8bacd0', + 3.0590169943749475, + '#769fc8', + 3.338525491562421, + '#6092c0', + ], + 'fill-opacity': 1, + }); + }); + }); +} 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/monitoring/enable_monitoring/index.js b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js index fefad214736e6..70f7e0559d034 100644 --- a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js @@ -10,6 +10,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['monitoring', 'common', 'header']); const esSupertest = getService('esSupertest'); const noData = getService('monitoringNoData'); + const testSubjects = getService('testSubjects'); const clusterOverview = getService('monitoringClusterOverview'); const retry = getService('retry'); @@ -46,8 +47,10 @@ export default function ({ getService, getPageObjects }) { }); // Here we are checking that once Monitoring is enabled, - //it moves on to the cluster overview page. - await retry.try(async () => { + // it moves on to the cluster overview page. + await retry.tryForTime(10000, async () => { + // Click the refresh button + await testSubjects.click('querySubmitButton'); expect(await clusterOverview.isOnClusterOverview()).to.be(true); }); }); diff --git a/x-pack/test/functional/apps/monitoring/time_filter.js b/x-pack/test/functional/apps/monitoring/time_filter.js index 11557d995218e..127c7d8889bc4 100644 --- a/x-pack/test/functional/apps/monitoring/time_filter.js +++ b/x-pack/test/functional/apps/monitoring/time_filter.js @@ -7,8 +7,6 @@ import expect from '@kbn/expect'; import { getLifecycleMethods } from './_get_lifecycle_methods'; -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['header', 'timePicker']); const testSubjects = getService('testSubjects'); @@ -36,12 +34,9 @@ export default function ({ getService, getPageObjects }) { expect(isLoading).to.be(true); }); - it('should send another request when changing the time picker', async () => { - /** - * TODO: The value should either be removed or lowered after: - * https://github.com/elastic/kibana/issues/72997 is resolved - */ - await delay(3000); + // TODO: [cr] I'm not sure this test is any better than the above one, we might need to rely solely on unit tests + // for this functionality + it.skip('should send another request when changing the time picker', async () => { await PageObjects.timePicker.setAbsoluteRange( 'Aug 15, 2016 @ 21:00:00.000', 'Aug 16, 2016 @ 00:00:00.000' 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/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 0f1fd3c09d706..f756d73484198 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -1008,6 +1008,40 @@ } } +{ + "type": "doc", + "value": { + "id": "map:bff99716-e3dc-11ea-87d0-0242ac130003", + "index": ".kibana", + "source": { + "map" : { + "description":"shapes with mvt scaling", + "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"76b9fc1d-1e8a-4d2f-9f9e-6ba2b19f24bb\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geometry\",\"filterByMapBounds\":true,\"scalingType\":\"MVT\",\"topHitsSize\":1,\"id\":\"97f8555e-8db0-4bd8-8b18-22e32f468667\",\"type\":\"ES_SEARCH\",\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"caffa63a-ebfb-466d-8ff6-d797975b88ab\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"prop1\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":1},\"type\":\"ORDINAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"TILED_VECTOR\",\"joins\":[]}]", + "mapStateJSON":"{\"zoom\":3.75,\"center\":{\"lon\":80.01106,\"lat\":3.65009},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "title":"geo_shape_mvt", + "uiStateJSON":"{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "id":"561253e0-f731-11e8-8487-11b9dd924f96", + "name":"layer_1_source_index_pattern", + "type":"index-pattern" + } + ], + "migrationVersion" : { + "map" : "7.9.0" + }, + "updated_at" : "2020-08-10T18:27:39.805Z" + } + } +} + + + + + + { "type": "doc", "value": { diff --git a/x-pack/test/functional/es_archives/monitoring/apm/mappings.json b/x-pack/test/functional/es_archives/monitoring/apm/mappings.json index d5f68e6642a2d..df00072edd669 100644 --- a/x-pack/test/functional/es_archives/monitoring/apm/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/apm/mappings.json @@ -1,719 +1,311 @@ { - "type": "index", - "value": { - "aliases": {}, - "index": ".monitoring-beats-6-2018.08.31", - "mappings": { - "dynamic": false, - "properties": { - "beats_state": { - "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" + "type": "index", + "value": { + "aliases": { + }, + "index": ".monitoring-beats-6-2018.08.31", + "mappings": { + "dynamic": false, + "properties": { + "beats_state": { + "properties": { + "timestamp": { + "format": "date_time", + "type": "date" + } + } + }, + "beats_stats": { + "properties": { + "beat": { + "properties": { + "type": { + "type": "keyword" + }, + "uuid": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "metrics": { + "properties": { + "apm-server": { + "properties": { + "processor": { + "properties": { + "error": { + "properties": { + "transformations": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "transformations": { + "type": "long" + } + } + }, + "span": { + "properties": { + "transformations": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "transformations": { + "type": "long" + } + } + } + } + }, + "server": { + "properties": { + "request": { + "properties": { + "count": { + "type": "long" + } + } + }, + "response": { + "properties": { + "count": { + "type": "long" + }, + "errors": { + "properties": { + "closed": { + "type": "long" }, - "name": { - "type": "keyword" + "concurrency": { + "type": "long" }, - "type": { - "type": "keyword" + "decode": { + "type": "long" }, - "uuid": { - "type": "keyword" + "forbidden": { + "type": "long" }, - "version": { - "type": "keyword" - } - } - }, - "state": { - "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } + "internal": { + "type": "long" + }, + "method": { + "type": "long" }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } + "queue": { + "type": "long" }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } + "ratelimit": { + "type": "long" }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } + "toolarge": { + "type": "long" }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } + "unauthorized": { + "type": "long" }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } + "validate": { + "type": "long" } + } + }, + "valid": { + "properties": { + "accepted": { + "type": "long" + }, + "ok": { + "type": "long" + } + } } - }, - "timestamp": { - "format": "date_time", - "type": "date" + } } + } } + } }, - "beats_stats": { - "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } + "beat": { + "properties": { + "cpu": { + "properties": { + "total": { + "properties": { + "value": { + "type": "long" } + } + } + } + }, + "handles": { + "properties": { + "open": { + "type": "long" + } + } + }, + "info": { + "properties": { + "ephemeral_id": { + "type": "keyword" + } + } + }, + "memstats": { + "properties": { + "gc_next": { + "type": "long" }, - "metrics": { - "properties": { - "beat": { - "properties": { - "cpu": { - "properties": { - "system": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "value": { - "type": "long" - }, - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "user": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - } - } - }, - "info": { - "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "uptime": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "memstats": { - "properties": { - "gc_next": { - "type": "long" - }, - "memory_alloc": { - "type": "long" - }, - "memory_total": { - "type": "long" - }, - "rss": { - "type": "long" - } - } - }, - "handles": { - "properties": { - "open": { - "type": "long" - }, - "limit": { - "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - } - } - } - } - }, - "apm-server": { - "properties": { - "server": { - "properties": { - "request": { - "properties": { - "count": { - "type": "long" - } - } - }, - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "response": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "properties": { - "count": { - "type": "long" - }, - "toolarge": { - "type": "long" - }, - "validate": { - "type": "long" - }, - "ratelimit": { - "type": "long" - }, - "queue": { - "type": "long" - }, - "closed": { - "type": "long" - }, - "forbidden": { - "type": "long" - }, - "concurrency": { - "type": "long" - }, - "unauthorized": { - "type": "long" - }, - "internal": { - "type": "long" - }, - "decode": { - "type": "long" - }, - "method": { - "type": "long" - } - } - }, - "valid": { - "properties": { - "ok": { - "type": "long" - }, - "accepted": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - } - } - }, - "decoder": { - "properties": { - "deflate": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "gzip": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "uncompressed": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "reader": { - "properties": { - "size": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "missing-content-length": { - "properties": { - "count": { - "type": "long" - } - } - } - } - }, - "processor": { - "properties": { - "metric": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - } - } - }, - "sourcemap": { - "properties": { - "counter": { - "type": "long" - }, - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - }, - "transaction": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "transactions": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "error": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "errors": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "span": { - "properties": { - "transformations": { - "type": "long" - } - } - } - } - } - } - }, - "libbeat": { - "properties": { - "config": { - "properties": { - "module": { - "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { - "type": "long" - } - } - }, - "reloads": { - "type": "long" - } - } - }, - "output": { - "properties": { - "events": { - "properties": { - "acked": { - "type": "long" - }, - "active": { - "type": "long" - }, - "batches": { - "type": "long" - }, - "dropped": { - "type": "long" - }, - "duplicates": { - "type": "long" - }, - "failed": { - "type": "long" - }, - "total": { - "type": "long" - }, - "toomany": { - "type": "long" - } - } - }, - "read": { - "properties": { - "bytes": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "write": { - "properties": { - "bytes": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - } - } - }, - "pipeline": { - "properties": { - "clients": { - "type": "long" - }, - "events": { - "properties": { - "active": { - "type": "long" - }, - "dropped": { - "type": "long" - }, - "failed": { - "type": "long" - }, - "filtered": { - "type": "long" - }, - "published": { - "type": "long" - }, - "retry": { - "type": "long" - }, - "total": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } - } - } - } - } - }, - "system": { - "properties": { - "load": { - "properties": { - "1": { - "type": "double" - }, - "5": { - "type": "double" - }, - "15": { - "type": "double" - }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "5": { - "type": "double" - }, - "15": { - "type": "double" - } - } - } - } - } - } - } - } + "memory_alloc": { + "type": "long" }, - "tags": { - "type": "keyword" + "memory_total": { + "type": "long" }, - "timestamp": { - "format": "date_time", - "type": "date" + "rss": { + "type": "long" } + } } + } }, - "cluster_uuid": { - "type": "keyword" - }, - "interval_ms": { - "type": "long" - }, - "source_node": { - "properties": { - "host": { - "type": "keyword" + "libbeat": { + "properties": { + "output": { + "properties": { + "events": { + "properties": { + "acked": { + "type": "long" + }, + "active": { + "type": "long" + }, + "dropped": { + "type": "long" + }, + "failed": { + "type": "long" + }, + "total": { + "type": "long" + } + } }, - "ip": { - "type": "keyword" + "read": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } }, - "name": { - "type": "keyword" + "write": { + "properties": { + "bytes": { + "type": "long" + }, + "errors": { + "type": "long" + } + } + } + } + }, + "pipeline": { + "properties": { + "events": { + "properties": { + "dropped": { + "type": "long" + }, + "failed": { + "type": "long" + }, + "published": { + "type": "long" + }, + "retry": { + "type": "long" + }, + "total": { + "type": "long" + } + } + } + } + } + } + }, + "system": { + "properties": { + "load": { + "properties": { + "1": { + "type": "double" }, - "transport_address": { - "type": "keyword" + "15": { + "type": "double" }, - "uuid": { - "type": "keyword" + "5": { + "type": "double" } + } } - }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "type": { - "type": "keyword" + } } + } + }, + "timestamp": { + "format": "date_time", + "type": "date" } + } + }, + "cluster_uuid": { + "type": "keyword" }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "codec": "best_compression", - "format": "6", - "number_of_replicas": "0", - "number_of_shards": "1" + "metricset": { + "properties": { + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } + } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "codec": "best_compression", + "format": "6", + "number_of_replicas": "0", + "number_of_shards": "1" + } } + } } \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/monitoring/basic_6.3.x/mappings.json b/x-pack/test/functional/es_archives/monitoring/basic_6.3.x/mappings.json index 9abc5c6381e45..4909b73909186 100644 --- a/x-pack/test/functional/es_archives/monitoring/basic_6.3.x/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/basic_6.3.x/mappings.json @@ -1,7 +1,8 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-kibana-6-2018.07.23", "mappings": { "dynamic": false, @@ -9,95 +10,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -105,47 +32,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -155,36 +63,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -192,9 +73,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -208,10 +86,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -230,121 +122,54 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-es-6-2018.07.23", "mappings": { "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -359,125 +184,43 @@ } } }, - "fielddata": { + "indexing": { "properties": { - "memory_size_in_bytes": { + "index_time_in_millis": { "type": "long" }, - "evictions": { + "index_total": { + "type": "long" + }, + "throttle_time_in_millis": { "type": "long" } } }, - "store": { + "merges": { "properties": { - "size_in_bytes": { + "total_size_in_bytes": { "type": "long" } } }, - "indexing": { + "refresh": { "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - }, - "throttle_time_in_millis": { + "total_time_in_millis": { "type": "long" } } }, - "merges": { + "segments": { "properties": { - "total_size_in_bytes": { - "type": "long" + "count": { + "type": "integer" } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - }, - "segments": { - "properties": { - "count": { - "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - } - } - }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -486,36 +229,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -534,14 +260,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -550,24 +274,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -577,41 +292,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -620,68 +335,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -702,15 +441,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -718,15 +448,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -745,116 +466,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -870,16 +572,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -891,12 +587,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -906,12 +596,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -921,71 +605,10 @@ } } }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, "thread_pool": { "properties": { "bulk": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -996,9 +619,6 @@ }, "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1009,22 +629,6 @@ }, "index": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1035,22 +639,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1073,246 +661,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/mappings.json b/x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/mappings.json index f616ffc1d3aec..a8017ee6631b0 100644 --- a/x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/beats-with-restarted-instance/mappings.json @@ -7,105 +7,6 @@ "properties": { "beats_state": { "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "state": { - "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, "timestamp": { "format": "date_time", "type": "date" @@ -116,12 +17,6 @@ "properties": { "beat": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -135,108 +30,40 @@ }, "metrics": { "properties": { - "beat": { + "apm-server": { "properties": { - "cpu": { + "processor": { "properties": { - "system": { + "error": { "properties": { - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } }, - "total": { + "metric": { "properties": { - "value": { - "type": "long" - }, - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } }, - "user": { + "span": { "properties": { - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } - } - } - }, - "info": { - "properties": { - "ephemeral_id": { - "type": "keyword" }, - "uptime": { + "transaction": { "properties": { - "ms": { + "transformations": { "type": "long" } } } } }, - "memstats": { - "properties": { - "gc_next": { - "type": "long" - }, - "memory_alloc": { - "type": "long" - }, - "memory_total": { - "type": "long" - }, - "rss": { - "type": "long" - } - } - }, - "handles": { - "properties": { - "open": { - "type": "long" - }, - "limit": { - "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - } - } - } - } - }, - "apm-server": { - "properties": { "server": { "properties": { "request": { @@ -246,17 +73,6 @@ } } }, - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "response": { "properties": { "count": { @@ -264,53 +80,47 @@ }, "errors": { "properties": { - "count": { - "type": "long" - }, - "toolarge": { + "closed": { "type": "long" }, - "validate": { + "concurrency": { "type": "long" }, - "ratelimit": { + "decode": { "type": "long" }, - "queue": { + "forbidden": { "type": "long" }, - "closed": { + "internal": { "type": "long" }, - "forbidden": { + "method": { "type": "long" }, - "concurrency": { + "queue": { "type": "long" }, - "unauthorized": { + "ratelimit": { "type": "long" }, - "internal": { + "toolarge": { "type": "long" }, - "decode": { + "unauthorized": { "type": "long" }, - "method": { + "validate": { "type": "long" } } }, "valid": { "properties": { - "ok": { - "type": "long" - }, "accepted": { "type": "long" }, - "count": { + "ok": { "type": "long" } } @@ -318,296 +128,109 @@ } } } - }, - "decoder": { + } + } + }, + "beat": { + "properties": { + "cpu": { "properties": { - "deflate": { + "total": { "properties": { - "content-length": { - "type": "long" - }, - "count": { + "value": { "type": "long" } } + } + } + }, + "handles": { + "properties": { + "open": { + "type": "long" + } + } + }, + "info": { + "properties": { + "ephemeral_id": { + "type": "keyword" + } + } + }, + "memstats": { + "properties": { + "gc_next": { + "type": "long" + }, + "memory_alloc": { + "type": "long" + }, + "memory_total": { + "type": "long" }, - "gzip": { + "rss": { + "type": "long" + } + } + } + } + }, + "libbeat": { + "properties": { + "output": { + "properties": { + "events": { "properties": { - "content-length": { + "acked": { "type": "long" }, - "count": { + "active": { "type": "long" - } - } - }, - "uncompressed": { - "properties": { - "content-length": { + }, + "dropped": { "type": "long" }, - "count": { + "failed": { + "type": "long" + }, + "total": { "type": "long" } } }, - "reader": { + "read": { "properties": { - "size": { + "bytes": { "type": "long" }, - "count": { + "errors": { "type": "long" } } }, - "missing-content-length": { + "write": { "properties": { - "count": { + "bytes": { + "type": "long" + }, + "errors": { "type": "long" } } } } }, - "processor": { + "pipeline": { "properties": { - "metric": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - } - } - }, - "sourcemap": { - "properties": { - "counter": { - "type": "long" - }, - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - }, - "transaction": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "transactions": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "error": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "errors": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "span": { - "properties": { - "transformations": { - "type": "long" - } - } - } - } - } - } - }, - "libbeat": { - "properties": { - "config": { - "properties": { - "module": { - "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { - "type": "long" - } - } - }, - "reloads": { - "type": "long" - } - } - }, - "output": { - "properties": { - "events": { - "properties": { - "acked": { - "type": "long" - }, - "active": { - "type": "long" - }, - "batches": { - "type": "long" - }, - "dropped": { - "type": "long" - }, - "duplicates": { - "type": "long" - }, - "failed": { - "type": "long" - }, - "total": { - "type": "long" - }, - "toomany": { - "type": "long" - } - } - }, - "read": { - "properties": { - "bytes": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "write": { - "properties": { - "bytes": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - } - } - }, - "pipeline": { - "properties": { - "clients": { - "type": "long" - }, "events": { "properties": { - "active": { - "type": "long" - }, "dropped": { "type": "long" }, "failed": { "type": "long" }, - "filtered": { - "type": "long" - }, "published": { "type": "long" }, @@ -618,13 +241,6 @@ "type": "long" } } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } } } } @@ -637,24 +253,11 @@ "1": { "type": "double" }, - "5": { - "type": "double" - }, "15": { "type": "double" }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "5": { - "type": "double" - }, - "15": { - "type": "double" - } - } + "5": { + "type": "double" } } } @@ -662,9 +265,6 @@ } } }, - "tags": { - "type": "keyword" - }, "timestamp": { "format": "date_time", "type": "date" @@ -674,25 +274,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -725,115 +316,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -848,29 +371,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -885,44 +391,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -931,42 +402,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -975,36 +416,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -1023,14 +447,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -1039,24 +461,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -1066,41 +479,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -1109,68 +522,92 @@ } } }, - "cluster_stats": { - "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" - } - } - }, - "cluster_state": { - "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" - } - } - }, - "node_stats": { + "indices_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "_all": { "properties": { - "docs": { + "primaries": { "properties": { - "count": { - "type": "long" + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "job_stats": { + "properties": { + "job_id": { + "type": "keyword" + } + } + }, + "node_stats": { + "properties": { + "fs": { + "properties": { + "io_stats": { + "properties": { + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -1191,15 +628,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1207,15 +635,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1234,116 +653,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1359,16 +759,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -1380,12 +774,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -1395,12 +783,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -1410,398 +792,97 @@ } } }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "thread_pool": { - "properties": { - "bulk": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "get": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "index": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "search": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "write": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - } - } - } - } - }, - "index_recovery": { - "type": "object" - }, - "shard": { - "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, - "index": { - "type": "keyword" - }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, - "node": { - "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" - }, - "state": { - "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - } - } - }, - "ccr_stats": { - "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { - "type": "keyword" - }, - "follower_index": { - "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", + "thread_pool": { "properties": { - "from_seq_no": { - "type": "long" + "bulk": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } }, - "retries": { - "type": "integer" + "get": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } }, - "exception": { - "type": "object", + "index": { "properties": { - "type": { - "type": "keyword" + "queue": { + "type": "integer" }, - "reason": { - "type": "text" + "rejected": { + "type": "long" + } + } + }, + "search": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" } } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" }, - "reason": { - "type": "text" + "write": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } } } } } }, - "ccr_auto_follow_stats": { + "shard": { "properties": { - "number_of_failed_follow_indices": { - "type": "long" + "index": { + "type": "keyword" }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" + "node": { + "type": "keyword" }, - "number_of_successful_follow_indices": { - "type": "long" + "primary": { + "type": "boolean" }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } + "state": { + "type": "keyword" + } + } + }, + "source_node": { + "properties": { + "name": { + "type": "keyword" }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } + "uuid": { + "type": "keyword" } } + }, + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1824,24 +905,9 @@ "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -1863,6 +929,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, @@ -1888,95 +969,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -1984,47 +991,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -2034,36 +1022,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -2071,9 +1032,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -2087,10 +1045,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/beats/mappings.json b/x-pack/test/functional/es_archives/monitoring/beats/mappings.json index e9e03d6ff2a16..2ea26547e767b 100644 --- a/x-pack/test/functional/es_archives/monitoring/beats/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/beats/mappings.json @@ -6,115 +6,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -129,29 +61,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -166,44 +81,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -212,42 +92,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -256,36 +106,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -304,14 +137,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -320,24 +151,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -347,41 +169,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -390,68 +212,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -472,15 +318,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -488,15 +325,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -515,116 +343,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -640,16 +449,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -661,12 +464,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -676,12 +473,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -691,110 +482,30 @@ } } }, - "jvm": { + "thread_pool": { "properties": { - "mem": { + "bulk": { "properties": { - "heap_used_in_bytes": { - "type": "long" + "queue": { + "type": "integer" }, - "heap_used_percent": { - "type": "half_float" + "rejected": { + "type": "long" + } + } + }, + "get": { + "properties": { + "queue": { + "type": "integer" }, - "heap_max_in_bytes": { + "rejected": { "type": "long" } } }, - "gc": { + "index": { "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "thread_pool": { - "properties": { - "bulk": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "get": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "index": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -805,22 +516,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -843,368 +538,64 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { - "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { - "type": "keyword" - }, - "follower_index": { - "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "format": "6", - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} - -{ - "type": "index", - "value": { - "index": ".monitoring-beats-6-2017.12.19", - "mappings": { - "dynamic": false, - "properties": { - "beats_state": { + "source_node": { "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "state": { - "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } + "name": { + "type": "keyword" }, + "uuid": { + "type": "keyword" + } + } + }, + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "format": "6", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "index": ".monitoring-beats-6-2017.12.19", + "mappings": { + "dynamic": false, + "properties": { + "beats_state": { + "properties": { "timestamp": { "format": "date_time", "type": "date" @@ -1215,12 +606,6 @@ "properties": { "beat": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -1234,108 +619,40 @@ }, "metrics": { "properties": { - "beat": { + "apm-server": { "properties": { - "cpu": { + "processor": { "properties": { - "system": { + "error": { "properties": { - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } }, - "total": { + "metric": { "properties": { - "value": { - "type": "long" - }, - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } }, - "user": { + "span": { "properties": { - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } - } - } - }, - "info": { - "properties": { - "ephemeral_id": { - "type": "keyword" }, - "uptime": { + "transaction": { "properties": { - "ms": { + "transformations": { "type": "long" } } } } }, - "memstats": { - "properties": { - "gc_next": { - "type": "long" - }, - "memory_alloc": { - "type": "long" - }, - "memory_total": { - "type": "long" - }, - "rss": { - "type": "long" - } - } - }, - "handles": { - "properties": { - "open": { - "type": "long" - }, - "limit": { - "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - } - } - } - } - }, - "apm-server": { - "properties": { "server": { "properties": { "request": { @@ -1345,17 +662,6 @@ } } }, - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "response": { "properties": { "count": { @@ -1363,247 +669,50 @@ }, "errors": { "properties": { - "count": { - "type": "long" - }, - "toolarge": { - "type": "long" - }, - "validate": { - "type": "long" - }, - "ratelimit": { - "type": "long" - }, - "queue": { - "type": "long" - }, "closed": { "type": "long" }, - "forbidden": { - "type": "long" - }, "concurrency": { "type": "long" }, - "unauthorized": { - "type": "long" - }, - "internal": { - "type": "long" - }, "decode": { "type": "long" }, - "method": { - "type": "long" - } - } - }, - "valid": { - "properties": { - "ok": { - "type": "long" - }, - "accepted": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - } - } - }, - "decoder": { - "properties": { - "deflate": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "gzip": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "uncompressed": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "reader": { - "properties": { - "size": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "missing-content-length": { - "properties": { - "count": { - "type": "long" - } - } - } - } - }, - "processor": { - "properties": { - "metric": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { + "forbidden": { "type": "long" }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - } - } - }, - "sourcemap": { - "properties": { - "counter": { - "type": "long" - }, - "decoding": { - "properties": { - "errors": { + "internal": { "type": "long" }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { + "method": { "type": "long" }, - "count": { - "type": "long" - } - } - } - } - }, - "transaction": { - "properties": { - "decoding": { - "properties": { - "errors": { + "queue": { "type": "long" }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { + "ratelimit": { "type": "long" }, - "count": { + "toolarge": { "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "transactions": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "error": { - "properties": { - "decoding": { - "properties": { - "errors": { + }, + "unauthorized": { "type": "long" }, - "count": { + "validate": { "type": "long" } } }, - "validation": { + "valid": { "properties": { - "errors": { + "accepted": { "type": "long" }, - "count": { + "ok": { "type": "long" } } - }, - "transformations": { - "type": "long" - }, - "errors": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "span": { - "properties": { - "transformations": { - "type": "long" } } } @@ -1611,28 +720,53 @@ } } }, - "libbeat": { + "beat": { "properties": { - "config": { + "cpu": { "properties": { - "module": { + "total": { "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { + "value": { "type": "long" } } - }, - "reloads": { + } + } + }, + "handles": { + "properties": { + "open": { "type": "long" } } }, + "info": { + "properties": { + "ephemeral_id": { + "type": "keyword" + } + } + }, + "memstats": { + "properties": { + "gc_next": { + "type": "long" + }, + "memory_alloc": { + "type": "long" + }, + "memory_total": { + "type": "long" + }, + "rss": { + "type": "long" + } + } + } + } + }, + "libbeat": { + "properties": { "output": { "properties": { "events": { @@ -1643,23 +777,14 @@ "active": { "type": "long" }, - "batches": { - "type": "long" - }, "dropped": { "type": "long" }, - "duplicates": { - "type": "long" - }, "failed": { "type": "long" }, "total": { "type": "long" - }, - "toomany": { - "type": "long" } } }, @@ -1673,9 +798,6 @@ } } }, - "type": { - "type": "keyword" - }, "write": { "properties": { "bytes": { @@ -1690,23 +812,14 @@ }, "pipeline": { "properties": { - "clients": { - "type": "long" - }, "events": { "properties": { - "active": { - "type": "long" - }, "dropped": { "type": "long" }, "failed": { "type": "long" }, - "filtered": { - "type": "long" - }, "published": { "type": "long" }, @@ -1717,13 +830,6 @@ "type": "long" } } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } } } } @@ -1736,24 +842,11 @@ "1": { "type": "double" }, - "5": { - "type": "double" - }, "15": { "type": "double" }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "5": { - "type": "double" - }, - "15": { - "type": "double" - } - } + "5": { + "type": "double" } } } @@ -1761,9 +854,6 @@ } } }, - "tags": { - "type": "keyword" - }, "timestamp": { "format": "date_time", "type": "date" @@ -1773,25 +863,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -1823,95 +904,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -1919,47 +926,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -1969,36 +957,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -2006,9 +967,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -2022,10 +980,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/ccr/mappings.json b/x-pack/test/functional/es_archives/monitoring/ccr/mappings.json index c034852554a44..4a941d9bd0d77 100644 --- a/x-pack/test/functional/es_archives/monitoring/ccr/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/ccr/mappings.json @@ -1,121 +1,54 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-es-6-2018.09.19", "mappings": { "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -130,29 +63,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -167,44 +83,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -213,42 +94,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -257,36 +108,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -305,14 +139,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -321,24 +153,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -348,41 +171,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -391,68 +214,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -473,15 +320,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -489,15 +327,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -516,116 +345,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -641,16 +451,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -662,12 +466,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -677,12 +475,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -692,71 +484,10 @@ } } }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, "thread_pool": { "properties": { "bulk": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -767,9 +498,6 @@ }, "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -780,22 +508,6 @@ }, "index": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -806,22 +518,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -844,246 +540,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/logs/data.json.gz b/x-pack/test/functional/es_archives/monitoring/logs/data.json.gz index 7f48fca2ea8a8..851db319e9ecf 100644 Binary files a/x-pack/test/functional/es_archives/monitoring/logs/data.json.gz and b/x-pack/test/functional/es_archives/monitoring/logs/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/monitoring/logs/mappings.json b/x-pack/test/functional/es_archives/monitoring/logs/mappings.json index 6dbecb8d503fd..b392c18e4e49b 100644 --- a/x-pack/test/functional/es_archives/monitoring/logs/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/logs/mappings.json @@ -8,93 +8,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -104,112 +25,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -224,16 +63,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -254,22 +83,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -277,66 +90,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -351,18 +108,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -390,17 +137,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -413,17 +151,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -491,13 +220,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -507,33 +229,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -555,58 +257,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -614,13 +268,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -628,15 +275,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -648,12 +289,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -661,18 +296,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -693,33 +318,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -769,13 +376,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -824,15 +424,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -842,9 +436,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -862,26 +453,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -889,14 +464,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -912,12 +481,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -930,22 +493,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -956,9 +503,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -969,22 +513,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -995,22 +523,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1039,12 +551,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1052,22 +558,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } diff --git a/x-pack/test/functional/es_archives/monitoring/logs_multiple_clusters/mappings.json b/x-pack/test/functional/es_archives/monitoring/logs_multiple_clusters/mappings.json index ccea50f747800..2d6c2f885f831 100644 --- a/x-pack/test/functional/es_archives/monitoring/logs_multiple_clusters/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/logs_multiple_clusters/mappings.json @@ -8,96 +8,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "follower_aliases_version": { - "type": "long" - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -107,112 +25,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -227,16 +63,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -257,22 +83,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -280,66 +90,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -354,18 +108,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -393,17 +137,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -416,17 +151,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -494,13 +220,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -510,33 +229,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -558,58 +257,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -617,13 +268,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -631,15 +275,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -651,12 +289,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -664,18 +296,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -696,33 +318,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -772,13 +376,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -827,15 +424,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -845,9 +436,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -865,26 +453,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -892,14 +464,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -915,12 +481,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -933,22 +493,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -959,9 +503,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -972,22 +513,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -998,22 +523,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1042,12 +551,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1055,22 +558,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } diff --git a/x-pack/test/functional/es_archives/monitoring/logstash-pipelines/mappings.json b/x-pack/test/functional/es_archives/monitoring/logstash-pipelines/mappings.json index aa3aff8b5c41a..98303a4b29d67 100644 --- a/x-pack/test/functional/es_archives/monitoring/logstash-pipelines/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/logstash-pipelines/mappings.json @@ -8,83 +8,25 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, - "logstash_stats": { - "type": "object", + "logstash_state": { "properties": { - "logstash": { + "pipeline": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "version": { + "hash": { "type": "keyword" }, - "snapshot": { - "type": "boolean" - }, - "status": { + "id": { "type": "keyword" - }, - "pipeline": { - "properties": { - "workers": { - "type": "short" - }, - "batch_size": { - "type": "long" - } - } } } - }, + } + } + }, + "logstash_stats": { + "properties": { "events": { "properties": { - "filtered": { + "duration_in_millis": { "type": "long" }, "in": { @@ -92,48 +34,11 @@ }, "out": { "type": "long" - }, - "duration_in_millis": { - "type": "long" } } }, - "timestamp": { - "type": "date" - }, "jvm": { "properties": { - "uptime_in_millis": { - "type": "long" - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -141,50 +46,30 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } + }, + "uptime_in_millis": { + "type": "long" } } }, - "os": { + "logstash": { "properties": { - "cpu": { - "properties": { - "load_average": { - "properties": { - "1m": { - "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" - } - } - } - } + "uuid": { + "type": "keyword" }, + "version": { + "type": "keyword" + } + } + }, + "os": { + "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -199,103 +84,70 @@ } } } + }, + "cpuacct": { + "properties": { + "usage_nanos": { + "type": "long" + } + } } } - } - } - }, - "process": { - "properties": { + }, "cpu": { "properties": { - "percent": { - "type": "long" + "load_average": { + "properties": { + "15m": { + "type": "half_float" + }, + "1m": { + "type": "half_float" + }, + "5m": { + "type": "half_float" + } + } } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" } } }, "pipelines": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { - "in": { - "type": "long" - }, - "filtered": { - "type": "long" - }, - "out": { - "type": "long" - }, "duration_in_millis": { "type": "long" }, - "queue_push_duration_in_millis": { + "out": { "type": "long" } } }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, "queue": { "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" - }, "max_queue_size_in_bytes": { "type": "long" }, "queue_size_in_bytes": { "type": "long" + }, + "type": { + "type": "keyword" } } }, "vertices": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "pipeline_ephemeral_id": { - "type": "keyword" + "duration_in_millis": { + "type": "long" }, "events_in": { "type": "long" @@ -303,111 +155,63 @@ "events_out": { "type": "long" }, - "duration_in_millis": { - "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" + "id": { + "type": "keyword" }, - "long_counters": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - } + "pipeline_ephemeral_id": { + "type": "keyword" }, - "double_gauges": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - } + "queue_push_duration_in_millis": { + "type": "float" } - } - }, - "reloads": { + }, + "type": "nested" + } + }, + "type": "nested" + }, + "process": { + "properties": { + "cpu": { "properties": { - "failures": { - "type": "long" - }, - "successes": { + "percent": { "type": "long" } } } } }, - "workers": { - "type": "short" + "queue": { + "properties": { + "events_count": { + "type": "long" + } + } }, - "batch_size": { - "type": "integer" + "timestamp": { + "type": "date" } } }, - "logstash_state": { + "metricset": { "properties": { - "uuid": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "workers": { - "type": "short" - }, - "batch_size": { - "type": "integer" - }, - "format": { + "fields": { + "keyword": { + "ignore_above": 256, "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "representation": { - "enabled": false } - } + }, + "type": "text" } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -431,115 +235,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -554,29 +290,64 @@ } } }, - "fielddata": { + "indexing": { "properties": { - "memory_size_in_bytes": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { "type": "long" }, - "evictions": { + "throttle_time_in_millis": { + "type": "long" + } + } + }, + "merges": { + "properties": { + "total_size_in_bytes": { + "type": "long" + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } }, + "segments": { + "properties": { + "count": { + "type": "integer" + } + } + }, "store": { "properties": { "size_in_bytes": { "type": "long" } } + } + } + }, + "total": { + "properties": { + "fielddata": { + "properties": { + "memory_size_in_bytes": { + "type": "long" + } + } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -595,14 +366,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -611,24 +380,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -638,176 +398,85 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } } } - }, - "total": { + } + } + }, + "indices_stats": { + "properties": { + "_all": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "fielddata": { + "primaries": { "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - }, - "throttle_time_in_millis": { - "type": "long" - } - } - }, - "merges": { - "properties": { - "total_size_in_bytes": { - "type": "long" - } - } - }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } } } }, - "segments": { + "total": { "properties": { - "count": { - "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - } - } - }, - "refresh": { - "properties": { - "total_time_in_millis": { - "type": "long" + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } } } } @@ -815,68 +484,49 @@ } } }, - "cluster_stats": { - "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" - } - } - }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -897,15 +547,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -913,15 +554,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -940,116 +572,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1065,16 +678,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -1086,12 +693,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -1101,12 +702,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -1116,71 +711,10 @@ } } }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, "thread_pool": { "properties": { "bulk": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1191,9 +725,6 @@ }, "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1204,22 +735,6 @@ }, "index": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1230,22 +745,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1268,246 +767,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/logstash/changing_pipelines/mappings.json b/x-pack/test/functional/es_archives/monitoring/logstash/changing_pipelines/mappings.json index 28c3c0e1b9e87..3c7511f426e2c 100644 --- a/x-pack/test/functional/es_archives/monitoring/logstash/changing_pipelines/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/logstash/changing_pipelines/mappings.json @@ -71,96 +71,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "follower_aliases_version": { - "type": "long" - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -170,112 +88,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -290,16 +126,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -320,22 +146,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -343,66 +153,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -417,18 +171,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -456,17 +200,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -479,17 +214,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -557,13 +283,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -573,33 +292,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -621,58 +320,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -680,13 +331,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -694,15 +338,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -714,12 +352,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -727,18 +359,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -759,33 +381,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -835,13 +439,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -890,15 +487,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -908,9 +499,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -928,26 +516,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -955,14 +527,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -978,12 +544,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -996,22 +556,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1022,9 +566,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1035,22 +576,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1061,22 +586,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1105,12 +614,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1118,22 +621,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -1175,79 +665,27 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "logstash_state": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "pipeline": { "properties": { - "batch_size": { - "type": "integer" - }, - "ephemeral_id": { - "type": "keyword" - }, - "format": { - "type": "keyword" - }, "hash": { "type": "keyword" }, "id": { "type": "keyword" - }, - "representation": { - "enabled": false, - "type": "object" - }, - "version": { - "type": "keyword" - }, - "workers": { - "type": "short" } } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" } } }, "logstash_stats": { "properties": { - "batch_size": { - "type": "integer" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, "in": { "type": "long" }, @@ -1258,34 +696,6 @@ }, "jvm": { "properties": { - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -1293,9 +703,6 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } }, @@ -1306,34 +713,6 @@ }, "logstash": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "batch_size": { - "type": "long" - }, - "workers": { - "type": "short" - } - } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -1348,9 +727,6 @@ "properties": { "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1368,9 +744,6 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } @@ -1399,25 +772,13 @@ }, "pipelines": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, - "in": { - "type": "long" - }, "out": { "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" } } }, @@ -1429,9 +790,6 @@ }, "queue": { "properties": { - "events_count": { - "type": "long" - }, "max_queue_size_in_bytes": { "type": "long" }, @@ -1443,29 +801,8 @@ } } }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, "vertices": { "properties": { - "double_gauges": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - }, - "type": "nested" - }, "duration_in_millis": { "type": "long" }, @@ -1478,22 +815,11 @@ "id": { "type": "keyword" }, - "long_counters": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - }, - "type": "nested" - }, "pipeline_ephemeral_id": { "type": "keyword" }, "queue_push_duration_in_millis": { - "type": "long" + "type": "float" } }, "type": "nested" @@ -1509,12 +835,6 @@ "type": "long" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -1522,50 +842,24 @@ "properties": { "events_count": { "type": "long" - }, - "type": { - "type": "keyword" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" } } }, "timestamp": { "type": "date" - }, - "workers": { - "type": "short" } } }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/logstash_pipelines_multicluster/mappings.json b/x-pack/test/functional/es_archives/monitoring/logstash_pipelines_multicluster/mappings.json index f2a7cca4efd8b..de13ddf620d77 100644 --- a/x-pack/test/functional/es_archives/monitoring/logstash_pipelines_multicluster/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/logstash_pipelines_multicluster/mappings.json @@ -71,96 +71,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "follower_aliases_version": { - "type": "long" - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -170,112 +88,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -290,16 +126,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -320,22 +146,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -343,66 +153,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -417,18 +171,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -456,17 +200,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -479,17 +214,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -557,13 +283,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -573,33 +292,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -621,58 +320,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -680,13 +331,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -694,15 +338,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -714,12 +352,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -727,18 +359,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -759,33 +381,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -835,13 +439,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -890,15 +487,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -908,9 +499,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -928,26 +516,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -955,14 +527,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -978,12 +544,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -996,22 +556,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1022,9 +566,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1035,22 +576,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1061,22 +586,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1105,12 +614,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1118,22 +621,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -1175,79 +665,27 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "logstash_state": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "pipeline": { "properties": { - "batch_size": { - "type": "integer" - }, - "ephemeral_id": { - "type": "keyword" - }, - "format": { - "type": "keyword" - }, "hash": { "type": "keyword" }, "id": { "type": "keyword" - }, - "representation": { - "enabled": false, - "type": "object" - }, - "version": { - "type": "keyword" - }, - "workers": { - "type": "short" } } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" } } }, "logstash_stats": { "properties": { - "batch_size": { - "type": "integer" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, "in": { "type": "long" }, @@ -1258,34 +696,6 @@ }, "jvm": { "properties": { - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -1293,9 +703,6 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } }, @@ -1306,34 +713,6 @@ }, "logstash": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "batch_size": { - "type": "long" - }, - "workers": { - "type": "short" - } - } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -1348,9 +727,6 @@ "properties": { "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1368,9 +744,6 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } @@ -1399,25 +772,13 @@ }, "pipelines": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, - "in": { - "type": "long" - }, "out": { "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" } } }, @@ -1429,9 +790,6 @@ }, "queue": { "properties": { - "events_count": { - "type": "long" - }, "max_queue_size_in_bytes": { "type": "long" }, @@ -1443,29 +801,8 @@ } } }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, "vertices": { "properties": { - "double_gauges": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - }, - "type": "nested" - }, "duration_in_millis": { "type": "long" }, @@ -1478,22 +815,11 @@ "id": { "type": "keyword" }, - "long_counters": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - }, - "type": "nested" - }, "pipeline_ephemeral_id": { "type": "keyword" }, "queue_push_duration_in_millis": { - "type": "long" + "type": "float" } }, "type": "nested" @@ -1509,12 +835,6 @@ "type": "long" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -1522,50 +842,24 @@ "properties": { "events_count": { "type": "long" - }, - "type": { - "type": "keyword" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" } } }, "timestamp": { "type": "date" - }, - "workers": { - "type": "short" } } }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/multi-basic/mappings.json b/x-pack/test/functional/es_archives/monitoring/multi-basic/mappings.json index 75bb2602f1a15..76717c0a61bba 100644 --- a/x-pack/test/functional/es_archives/monitoring/multi-basic/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/multi-basic/mappings.json @@ -5,24 +5,9 @@ "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -44,6 +29,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, @@ -68,95 +68,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -164,47 +90,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -214,36 +121,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -251,9 +131,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -267,10 +144,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -293,136 +184,92 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { + "nodes_hash": { + "type": "integer" + } + } + }, + "cluster_uuid": { + "type": "keyword" + }, + "index_stats": { + "properties": { + "index": { + "type": "keyword" + }, + "primaries": { "properties": { - "primaries": { + "docs": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } + "count": { + "type": "long" + } + } + }, + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } + "index_total": { + "type": "long" }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } + "throttle_time_in_millis": { + "type": "long" } } }, - "total": { + "merges": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } + "total_size_in_bytes": { + "type": "long" } } - } - } - } - } - }, - "index_stats": { - "properties": { - "index": { - "type": "keyword" - }, - "primaries": { - "properties": { - "docs": { + }, + "refresh": { "properties": { - "count": { + "total_time_in_millis": { "type": "long" } } }, - "fielddata": { + "segments": { "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" + "count": { + "type": "integer" } } }, @@ -432,13 +279,24 @@ "type": "long" } } + } + } + }, + "total": { + "properties": { + "fielddata": { + "properties": { + "memory_size_in_bytes": { + "type": "long" + } + } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -457,14 +315,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -473,24 +329,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -500,79 +347,144 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } } } - }, - "total": { + } + } + }, + "indices_stats": { + "properties": { + "_all": { "properties": { - "docs": { + "primaries": { "properties": { - "count": { - "type": "long" + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } } } }, - "fielddata": { + "total": { "properties": { - "memory_size_in_bytes": { - "type": "long" + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } }, - "evictions": { - "type": "long" + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "job_stats": { + "properties": { + "job_id": { + "type": "keyword" + } + } + }, + "node_stats": { + "properties": { + "fs": { + "properties": { + "io_stats": { + "properties": { + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, - "store": { + "total": { "properties": { - "size_in_bytes": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { + "fielddata": { + "properties": { + "memory_size_in_bytes": { "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -580,26 +492,10 @@ } } }, - "merges": { - "properties": { - "total_size_in_bytes": { - "type": "long" - } - } - }, "query_cache": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -607,24 +503,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -634,284 +521,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "refresh": { - "properties": { - "total_time_in_millis": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } - } - } - }, - "cluster_stats": { - "properties": { - "nodes": { - "type": "object" }, - "indices": { - "type": "object" - } - } - }, - "cluster_state": { - "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" - } - } - }, - "node_stats": { - "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "jvm": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_time_in_millis": { - "type": "long" - }, - "index_total": { - "type": "long" - }, - "throttle_time_in_millis": { - "type": "long" - } - } - }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "gc": { "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" + "collectors": { + "properties": { + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } + }, + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } + } + } } } }, - "segments": { + "mem": { "properties": { - "count": { - "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { + "heap_max_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "heap_used_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" + "heap_used_percent": { + "type": "half_float" } } } } }, - "fs": { - "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { - "properties": { - "total": { - "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" - }, - "write_kilobytes": { - "type": "long" - } - } - } - } - } - } + "node_id": { + "type": "keyword" }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -927,16 +627,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -948,127 +642,28 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" - } - } - } - } - } - } - }, - "process": { - "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, - "cpu": { - "properties": { - "percent": { - "type": "half_float" - } - } - } - } - }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } } } } - } - } - } - }, - "thread_pool": { - "properties": { - "bulk": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "get": { + } + } + } + }, + "process": { + "properties": { + "cpu": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" + "percent": { + "type": "half_float" } } - }, - "index": { + } + } + }, + "thread_pool": { + "properties": { + "bulk": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1077,11 +672,8 @@ } } }, - "management": { + "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1090,11 +682,8 @@ } } }, - "search": { + "index": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1103,11 +692,8 @@ } } }, - "watcher": { + "search": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1130,246 +716,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1394,83 +775,25 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, - "logstash_stats": { - "type": "object", + "logstash_state": { "properties": { - "logstash": { + "pipeline": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, - "status": { + "hash": { + "type": "keyword" + }, + "id": { "type": "keyword" - }, - "pipeline": { - "properties": { - "workers": { - "type": "short" - }, - "batch_size": { - "type": "long" - } - } } } - }, + } + } + }, + "logstash_stats": { + "properties": { "events": { "properties": { - "filtered": { + "duration_in_millis": { "type": "long" }, "in": { @@ -1478,48 +801,11 @@ }, "out": { "type": "long" - }, - "duration_in_millis": { - "type": "long" } } }, - "timestamp": { - "type": "date" - }, "jvm": { "properties": { - "uptime_in_millis": { - "type": "long" - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -1527,50 +813,30 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } + }, + "uptime_in_millis": { + "type": "long" } } }, - "os": { + "logstash": { "properties": { - "cpu": { - "properties": { - "load_average": { - "properties": { - "1m": { - "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" - } - } - } - } + "uuid": { + "type": "keyword" }, + "version": { + "type": "keyword" + } + } + }, + "os": { + "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1585,103 +851,70 @@ } } } + }, + "cpuacct": { + "properties": { + "usage_nanos": { + "type": "long" + } + } } } - } - } - }, - "process": { - "properties": { + }, "cpu": { "properties": { - "percent": { - "type": "long" + "load_average": { + "properties": { + "15m": { + "type": "half_float" + }, + "1m": { + "type": "half_float" + }, + "5m": { + "type": "half_float" + } + } } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" } } }, "pipelines": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { - "in": { - "type": "long" - }, - "filtered": { - "type": "long" - }, - "out": { - "type": "long" - }, "duration_in_millis": { "type": "long" }, - "queue_push_duration_in_millis": { + "out": { "type": "long" } } }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, "queue": { "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" - }, "max_queue_size_in_bytes": { "type": "long" }, "queue_size_in_bytes": { "type": "long" + }, + "type": { + "type": "keyword" } } }, "vertices": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "pipeline_ephemeral_id": { - "type": "keyword" + "duration_in_millis": { + "type": "long" }, "events_in": { "type": "long" @@ -1689,111 +922,63 @@ "events_out": { "type": "long" }, - "duration_in_millis": { - "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" + "id": { + "type": "keyword" }, - "long_counters": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - } + "pipeline_ephemeral_id": { + "type": "keyword" }, - "double_gauges": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - } + "queue_push_duration_in_millis": { + "type": "float" } - } - }, - "reloads": { + }, + "type": "nested" + } + }, + "type": "nested" + }, + "process": { + "properties": { + "cpu": { "properties": { - "failures": { - "type": "long" - }, - "successes": { + "percent": { "type": "long" } } } } }, - "workers": { - "type": "short" + "queue": { + "properties": { + "events_count": { + "type": "long" + } + } }, - "batch_size": { - "type": "integer" + "timestamp": { + "type": "date" } } }, - "logstash_state": { + "metricset": { "properties": { - "uuid": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "workers": { - "type": "short" - }, - "batch_size": { - "type": "integer" - }, - "format": { - "type": "keyword" - }, - "version": { + "fields": { + "keyword": { + "ignore_above": 256, "type": "keyword" - }, - "representation": { - "enabled": false } - } + }, + "type": "text" } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/multicluster/mappings.json b/x-pack/test/functional/es_archives/monitoring/multicluster/mappings.json index 7ba6fd939afa8..7e7084698b788 100644 --- a/x-pack/test/functional/es_archives/monitoring/multicluster/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/multicluster/mappings.json @@ -1,121 +1,54 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-es-6-2017.08.15", "mappings": { "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -130,29 +63,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -167,44 +83,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -213,42 +94,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -257,36 +108,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -305,14 +139,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -321,24 +153,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -348,41 +171,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -391,68 +214,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -473,15 +320,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -489,15 +327,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -516,116 +345,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -641,16 +451,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -662,12 +466,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -677,12 +475,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -692,136 +484,40 @@ } } }, - "jvm": { + "thread_pool": { "properties": { - "mem": { + "bulk": { "properties": { - "heap_used_in_bytes": { + "queue": { + "type": "integer" + }, + "rejected": { "type": "long" + } + } + }, + "get": { + "properties": { + "queue": { + "type": "integer" }, - "heap_used_percent": { - "type": "half_float" + "rejected": { + "type": "long" + } + } + }, + "index": { + "properties": { + "queue": { + "type": "integer" }, - "heap_max_in_bytes": { + "rejected": { "type": "long" } } }, - "gc": { + "search": { "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "thread_pool": { - "properties": { - "bulk": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "get": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "index": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "search": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -844,246 +540,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1102,7 +593,8 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-logstash-6-2017.08.15", "mappings": { "dynamic": false, @@ -1110,83 +602,25 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, - "logstash_stats": { - "type": "object", + "logstash_state": { "properties": { - "logstash": { + "pipeline": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "version": { + "hash": { "type": "keyword" }, - "snapshot": { - "type": "boolean" - }, - "status": { + "id": { "type": "keyword" - }, - "pipeline": { - "properties": { - "workers": { - "type": "short" - }, - "batch_size": { - "type": "long" - } - } } } - }, + } + } + }, + "logstash_stats": { + "properties": { "events": { "properties": { - "filtered": { + "duration_in_millis": { "type": "long" }, "in": { @@ -1194,48 +628,11 @@ }, "out": { "type": "long" - }, - "duration_in_millis": { - "type": "long" } } }, - "timestamp": { - "type": "date" - }, "jvm": { "properties": { - "uptime_in_millis": { - "type": "long" - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -1243,50 +640,30 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } + }, + "uptime_in_millis": { + "type": "long" } } }, - "os": { + "logstash": { "properties": { - "cpu": { - "properties": { - "load_average": { - "properties": { - "1m": { - "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" - } - } - } - } + "uuid": { + "type": "keyword" }, + "version": { + "type": "keyword" + } + } + }, + "os": { + "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1301,103 +678,70 @@ } } } + }, + "cpuacct": { + "properties": { + "usage_nanos": { + "type": "long" + } + } } } - } - } - }, - "process": { - "properties": { + }, "cpu": { "properties": { - "percent": { - "type": "long" + "load_average": { + "properties": { + "15m": { + "type": "half_float" + }, + "1m": { + "type": "half_float" + }, + "5m": { + "type": "half_float" + } + } } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" } } }, "pipelines": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { - "in": { - "type": "long" - }, - "filtered": { - "type": "long" - }, - "out": { - "type": "long" - }, "duration_in_millis": { "type": "long" }, - "queue_push_duration_in_millis": { + "out": { "type": "long" } } }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, "queue": { "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" - }, "max_queue_size_in_bytes": { "type": "long" }, "queue_size_in_bytes": { "type": "long" + }, + "type": { + "type": "keyword" } } }, "vertices": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "pipeline_ephemeral_id": { - "type": "keyword" + "duration_in_millis": { + "type": "long" }, "events_in": { "type": "long" @@ -1405,111 +749,63 @@ "events_out": { "type": "long" }, - "duration_in_millis": { - "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" + "id": { + "type": "keyword" }, - "long_counters": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - } + "pipeline_ephemeral_id": { + "type": "keyword" }, - "double_gauges": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - } + "queue_push_duration_in_millis": { + "type": "float" } - } - }, - "reloads": { + }, + "type": "nested" + } + }, + "type": "nested" + }, + "process": { + "properties": { + "cpu": { "properties": { - "failures": { - "type": "long" - }, - "successes": { + "percent": { "type": "long" } } } } }, - "workers": { - "type": "short" + "queue": { + "properties": { + "events_count": { + "type": "long" + } + } }, - "batch_size": { - "type": "integer" + "timestamp": { + "type": "date" } } }, - "logstash_state": { + "metricset": { "properties": { - "uuid": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "workers": { - "type": "short" - }, - "batch_size": { - "type": "integer" - }, - "format": { + "fields": { + "keyword": { + "ignore_above": 256, "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "representation": { - "enabled": false } - } + }, + "type": "text" } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1528,7 +824,8 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-kibana-6-2017.08.15", "mappings": { "dynamic": false, @@ -1536,102 +833,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { - "usage": { - "properties": { - "index": { - "type": "keyword" - } - } + "concurrent_connections": { + "type": "long" }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -1639,47 +855,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -1689,36 +886,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -1726,9 +896,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -1742,10 +909,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1764,29 +945,15 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-alerts-6", "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -1808,6 +975,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/setup/collection/detect_beats/data.json.gz b/x-pack/test/functional/es_archives/monitoring/setup/collection/detect_beats/data.json.gz index 0548d40cd4f82..e4cffebf210ad 100644 Binary files a/x-pack/test/functional/es_archives/monitoring/setup/collection/detect_beats/data.json.gz and b/x-pack/test/functional/es_archives/monitoring/setup/collection/detect_beats/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/monitoring/setup/collection/detect_beats/mappings.json b/x-pack/test/functional/es_archives/monitoring/setup/collection/detect_beats/mappings.json index 2567ef3949ef9..c8c423463e477 100644 --- a/x-pack/test/functional/es_archives/monitoring/setup/collection/detect_beats/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/setup/collection/detect_beats/mappings.json @@ -8,93 +8,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -104,112 +25,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -224,16 +63,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -254,22 +83,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -277,66 +90,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -351,18 +108,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -390,17 +137,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -413,17 +151,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -491,13 +220,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -507,33 +229,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -555,58 +257,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -614,13 +268,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -628,15 +275,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -648,12 +289,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -661,18 +296,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -693,33 +318,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -769,13 +376,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -824,15 +424,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -842,9 +436,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -862,26 +453,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -889,14 +464,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -912,12 +481,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -930,22 +493,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -956,9 +503,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -969,22 +513,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -995,22 +523,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1039,12 +551,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1052,22 +558,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -1107,93 +600,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -1203,112 +617,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -1323,16 +655,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -1353,22 +675,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -1376,66 +682,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -1450,18 +700,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1489,17 +729,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1512,17 +743,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1590,13 +812,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -1606,33 +821,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -1643,69 +838,21 @@ "query_time_in_millis": { "type": "long" }, - "query_total": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "interval_ms": { - "type": "long" - }, - "job_stats": { - "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" + "query_total": { + "type": "long" + } + } + } + } } } - }, + } + } + }, + "job_stats": { + "properties": { "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -1713,13 +860,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -1727,15 +867,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -1747,12 +881,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -1760,18 +888,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1792,33 +910,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1868,13 +968,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -1923,15 +1016,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -1941,9 +1028,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1961,26 +1045,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -1988,14 +1056,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -2011,12 +1073,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -2029,22 +1085,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2055,9 +1095,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2068,22 +1105,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2094,22 +1115,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2138,12 +1143,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -2151,22 +1150,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -2208,63 +1194,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "kibana_stats": { "properties": { - "cloud": { - "properties": { - "id": { - "type": "keyword" - }, - "metadata": { - "type": "object" - }, - "name": { - "type": "keyword" - }, - "region": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "zone": { - "type": "keyword" - } - } - }, "concurrent_connections": { "type": "long" }, "kibana": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -2287,22 +1226,6 @@ "type": "half_float" } } - }, - "memory": { - "properties": { - "free_in_bytes": { - "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -2317,12 +1240,6 @@ "properties": { "size_limit": { "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" } } }, @@ -2330,9 +1247,6 @@ "type": "float" } } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -2341,9 +1255,6 @@ "disconnects": { "type": "long" }, - "status_codes": { - "type": "object" - }, "total": { "type": "long" } @@ -2359,49 +1270,15 @@ } } }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "timestamp": { - "type": "date" - } - } - }, - "source_node": { - "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "timestamp": { - "format": "date_time", "type": "date" }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } }, @@ -15291,4 +14168,4 @@ } } } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/monitoring/setup/collection/es_and_kibana_exclusive_mb/mappings.json b/x-pack/test/functional/es_archives/monitoring/setup/collection/es_and_kibana_exclusive_mb/mappings.json index ea61a4b7f325f..211e253ef83d4 100644 --- a/x-pack/test/functional/es_archives/monitoring/setup/collection/es_and_kibana_exclusive_mb/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/setup/collection/es_and_kibana_exclusive_mb/mappings.json @@ -9,105 +9,6 @@ "properties": { "beats_state": { "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "state": { - "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, "timestamp": { "format": "date_time", "type": "date" @@ -118,12 +19,6 @@ "properties": { "beat": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -139,146 +34,19 @@ "properties": { "apm-server": { "properties": { - "decoder": { - "properties": { - "deflate": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "gzip": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "missing-content-length": { - "properties": { - "count": { - "type": "long" - } - } - }, - "reader": { - "properties": { - "count": { - "type": "long" - }, - "size": { - "type": "long" - } - } - }, - "uncompressed": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - }, "processor": { "properties": { "error": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "errors": { - "type": "long" - }, - "frames": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } }, "metric": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - } - } - }, - "sourcemap": { - "properties": { - "counter": { - "type": "long" - }, - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } }, @@ -291,40 +59,8 @@ }, "transaction": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "frames": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "transactions": { - "type": "long" - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } } @@ -332,17 +68,6 @@ }, "server": { "properties": { - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "request": { "properties": { "count": { @@ -363,9 +88,6 @@ "concurrency": { "type": "long" }, - "count": { - "type": "long" - }, "decode": { "type": "long" }, @@ -400,9 +122,6 @@ "accepted": { "type": "long" }, - "count": { - "type": "long" - }, "ok": { "type": "long" } @@ -418,65 +137,17 @@ "properties": { "cpu": { "properties": { - "system": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "total": { "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - }, "value": { "type": "long" } } - }, - "user": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } } } }, "handles": { "properties": { - "limit": { - "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - }, "open": { "type": "long" } @@ -486,13 +157,6 @@ "properties": { "ephemeral_id": { "type": "keyword" - }, - "uptime": { - "properties": { - "ms": { - "type": "long" - } - } } } }, @@ -516,26 +180,6 @@ }, "libbeat": { "properties": { - "config": { - "properties": { - "module": { - "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { - "type": "long" - } - } - }, - "reloads": { - "type": "long" - } - } - }, "output": { "properties": { "events": { @@ -546,21 +190,12 @@ "active": { "type": "long" }, - "batches": { - "type": "long" - }, "dropped": { "type": "long" }, - "duplicates": { - "type": "long" - }, "failed": { "type": "long" }, - "toomany": { - "type": "long" - }, "total": { "type": "long" } @@ -576,9 +211,6 @@ } } }, - "type": { - "type": "keyword" - }, "write": { "properties": { "bytes": { @@ -593,23 +225,14 @@ }, "pipeline": { "properties": { - "clients": { - "type": "long" - }, "events": { "properties": { - "active": { - "type": "long" - }, "dropped": { "type": "long" }, "failed": { "type": "long" }, - "filtered": { - "type": "long" - }, "published": { "type": "long" }, @@ -620,13 +243,6 @@ "type": "long" } } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } } } } @@ -644,19 +260,6 @@ }, "5": { "type": "double" - }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "15": { - "type": "double" - }, - "5": { - "type": "double" - } - } } } } @@ -664,9 +267,6 @@ } } }, - "tags": { - "type": "keyword" - }, "timestamp": { "format": "date_time", "type": "date" @@ -676,25 +276,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -729,93 +320,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -825,112 +337,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -940,17 +370,7 @@ "properties": { "docs": { "properties": { - "count": { - "type": "long" - } - } - }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { + "count": { "type": "long" } } @@ -975,22 +395,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -998,66 +402,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -1072,18 +420,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1111,17 +449,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1134,17 +463,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1212,13 +532,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -1228,33 +541,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -1276,58 +569,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -1335,13 +580,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -1349,15 +587,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -1369,12 +601,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -1382,18 +608,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1414,33 +630,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1490,13 +688,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -1545,15 +736,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -1563,9 +748,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1583,26 +765,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -1610,14 +776,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -1633,12 +793,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -1651,22 +805,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1677,9 +815,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1690,22 +825,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1715,23 +834,7 @@ "type": "integer" }, "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" + "type": "long" } } }, @@ -1760,12 +863,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1773,22 +870,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -1830,63 +914,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "kibana_stats": { "properties": { - "cloud": { - "properties": { - "id": { - "type": "keyword" - }, - "metadata": { - "type": "object" - }, - "name": { - "type": "keyword" - }, - "region": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "zone": { - "type": "keyword" - } - } - }, "concurrent_connections": { "type": "long" }, "kibana": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -1909,22 +946,6 @@ "type": "half_float" } } - }, - "memory": { - "properties": { - "free_in_bytes": { - "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -1939,12 +960,6 @@ "properties": { "size_limit": { "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" } } }, @@ -1952,9 +967,6 @@ "type": "float" } } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -1963,9 +975,6 @@ "disconnects": { "type": "long" }, - "status_codes": { - "type": "object" - }, "total": { "type": "long" } @@ -1981,49 +990,15 @@ } } }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "timestamp": { - "type": "date" - } - } - }, - "source_node": { - "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "timestamp": { - "format": "date_time", "type": "date" }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } }, @@ -2060,79 +1035,27 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "logstash_state": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "pipeline": { "properties": { - "batch_size": { - "type": "integer" - }, - "ephemeral_id": { - "type": "keyword" - }, - "format": { - "type": "keyword" - }, "hash": { "type": "keyword" }, "id": { "type": "keyword" - }, - "representation": { - "enabled": false, - "type": "object" - }, - "version": { - "type": "keyword" - }, - "workers": { - "type": "short" } } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" } } }, "logstash_stats": { "properties": { - "batch_size": { - "type": "integer" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, "in": { "type": "long" }, @@ -2143,34 +1066,6 @@ }, "jvm": { "properties": { - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -2178,9 +1073,6 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } }, @@ -2191,34 +1083,6 @@ }, "logstash": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "batch_size": { - "type": "long" - }, - "workers": { - "type": "short" - } - } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -2233,9 +1097,6 @@ "properties": { "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -2253,9 +1114,6 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } @@ -2284,25 +1142,13 @@ }, "pipelines": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, - "in": { - "type": "long" - }, "out": { "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" } } }, @@ -2314,9 +1160,6 @@ }, "queue": { "properties": { - "events_count": { - "type": "long" - }, "max_queue_size_in_bytes": { "type": "long" }, @@ -2328,29 +1171,8 @@ } } }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, "vertices": { "properties": { - "double_gauges": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - }, - "type": "nested" - }, "duration_in_millis": { "type": "long" }, @@ -2363,22 +1185,11 @@ "id": { "type": "keyword" }, - "long_counters": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - }, - "type": "nested" - }, "pipeline_ephemeral_id": { "type": "keyword" }, "queue_push_duration_in_millis": { - "type": "long" + "type": "float" } }, "type": "nested" @@ -2394,12 +1205,6 @@ "type": "long" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -2407,50 +1212,24 @@ "properties": { "events_count": { "type": "long" - }, - "type": { - "type": "keyword" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" } } }, "timestamp": { "type": "date" - }, - "workers": { - "type": "short" } } }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/setup/collection/es_and_kibana_mb/mappings.json b/x-pack/test/functional/es_archives/monitoring/setup/collection/es_and_kibana_mb/mappings.json index ea9313fb87b6e..f8c55c60c4984 100644 --- a/x-pack/test/functional/es_archives/monitoring/setup/collection/es_and_kibana_mb/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/setup/collection/es_and_kibana_mb/mappings.json @@ -9,105 +9,6 @@ "properties": { "beats_state": { "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "state": { - "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, "timestamp": { "format": "date_time", "type": "date" @@ -118,12 +19,6 @@ "properties": { "beat": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -139,146 +34,19 @@ "properties": { "apm-server": { "properties": { - "decoder": { - "properties": { - "deflate": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "gzip": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "missing-content-length": { - "properties": { - "count": { - "type": "long" - } - } - }, - "reader": { - "properties": { - "count": { - "type": "long" - }, - "size": { - "type": "long" - } - } - }, - "uncompressed": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - }, "processor": { "properties": { "error": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "errors": { - "type": "long" - }, - "frames": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } }, "metric": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - } - } - }, - "sourcemap": { - "properties": { - "counter": { - "type": "long" - }, - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } }, @@ -291,40 +59,8 @@ }, "transaction": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "frames": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "transactions": { - "type": "long" - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } } @@ -332,17 +68,6 @@ }, "server": { "properties": { - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "request": { "properties": { "count": { @@ -363,9 +88,6 @@ "concurrency": { "type": "long" }, - "count": { - "type": "long" - }, "decode": { "type": "long" }, @@ -400,9 +122,6 @@ "accepted": { "type": "long" }, - "count": { - "type": "long" - }, "ok": { "type": "long" } @@ -418,65 +137,17 @@ "properties": { "cpu": { "properties": { - "system": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "total": { "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - }, "value": { "type": "long" } } - }, - "user": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } } } }, "handles": { "properties": { - "limit": { - "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - }, "open": { "type": "long" } @@ -486,13 +157,6 @@ "properties": { "ephemeral_id": { "type": "keyword" - }, - "uptime": { - "properties": { - "ms": { - "type": "long" - } - } } } }, @@ -516,26 +180,6 @@ }, "libbeat": { "properties": { - "config": { - "properties": { - "module": { - "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { - "type": "long" - } - } - }, - "reloads": { - "type": "long" - } - } - }, "output": { "properties": { "events": { @@ -546,21 +190,12 @@ "active": { "type": "long" }, - "batches": { - "type": "long" - }, "dropped": { "type": "long" }, - "duplicates": { - "type": "long" - }, "failed": { "type": "long" }, - "toomany": { - "type": "long" - }, "total": { "type": "long" } @@ -576,9 +211,6 @@ } } }, - "type": { - "type": "keyword" - }, "write": { "properties": { "bytes": { @@ -593,23 +225,14 @@ }, "pipeline": { "properties": { - "clients": { - "type": "long" - }, "events": { "properties": { - "active": { - "type": "long" - }, "dropped": { "type": "long" }, "failed": { "type": "long" }, - "filtered": { - "type": "long" - }, "published": { "type": "long" }, @@ -620,13 +243,6 @@ "type": "long" } } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } } } } @@ -644,19 +260,6 @@ }, "5": { "type": "double" - }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "15": { - "type": "double" - }, - "5": { - "type": "double" - } - } } } } @@ -664,9 +267,6 @@ } } }, - "tags": { - "type": "keyword" - }, "timestamp": { "format": "date_time", "type": "date" @@ -676,25 +276,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -729,93 +320,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -825,132 +337,40 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { "type": "keyword" }, "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "fielddata": { + "properties": { + "docs": { "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { + "count": { "type": "long" } } @@ -975,22 +395,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -998,66 +402,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -1072,18 +420,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1111,17 +449,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1134,17 +463,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1212,13 +532,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -1228,33 +541,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -1276,58 +569,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -1335,13 +580,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -1349,15 +587,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -1369,12 +601,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -1382,18 +608,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1414,33 +630,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1490,13 +688,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -1545,15 +736,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -1563,9 +748,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1583,26 +765,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -1610,14 +776,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -1630,108 +790,51 @@ "cpu": { "properties": { "percent": { - "type": "half_float" - } - } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" - } - } - }, - "thread_pool": { - "properties": { - "bulk": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "get": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" + "type": "half_float" } } - }, - "index": { + } + } + }, + "thread_pool": { + "properties": { + "bulk": { "properties": { "queue": { "type": "integer" }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, - "management": { + "get": { "properties": { "queue": { "type": "integer" }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, - "search": { + "index": { "properties": { "queue": { "type": "integer" }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, - "watcher": { + "search": { "properties": { "queue": { "type": "integer" }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1760,12 +863,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1773,22 +870,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -1828,93 +912,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -1924,112 +929,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -2044,16 +967,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -2074,22 +987,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -2097,66 +994,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -2171,18 +1012,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -2210,17 +1041,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -2233,17 +1055,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -2308,32 +1121,15 @@ "indices_stats": { "properties": { "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_time_in_millis": { - "type": "long" - }, - "index_total": { - "type": "long" - } - } - }, - "search": { + "properties": { + "primaries": { + "properties": { + "indexing": { "properties": { - "query_time_in_millis": { + "index_time_in_millis": { "type": "long" }, - "query_total": { + "index_total": { "type": "long" } } @@ -2342,18 +1138,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -2375,58 +1161,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -2434,13 +1172,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -2448,15 +1179,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -2468,12 +1193,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -2481,18 +1200,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -2513,33 +1222,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -2589,13 +1280,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -2644,15 +1328,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -2662,9 +1340,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -2682,26 +1357,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -2709,14 +1368,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -2732,12 +1385,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -2750,22 +1397,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2776,9 +1407,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2789,22 +1417,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2815,22 +1427,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2859,12 +1455,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -2872,22 +1462,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -2929,63 +1506,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "kibana_stats": { "properties": { - "cloud": { - "properties": { - "id": { - "type": "keyword" - }, - "metadata": { - "type": "object" - }, - "name": { - "type": "keyword" - }, - "region": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "zone": { - "type": "keyword" - } - } - }, "concurrent_connections": { "type": "long" }, "kibana": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -3008,22 +1538,6 @@ "type": "half_float" } } - }, - "memory": { - "properties": { - "free_in_bytes": { - "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -3038,12 +1552,6 @@ "properties": { "size_limit": { "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" } } }, @@ -3051,9 +1559,6 @@ "type": "float" } } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -3062,9 +1567,6 @@ "disconnects": { "type": "long" }, - "status_codes": { - "type": "object" - }, "total": { "type": "long" } @@ -3074,55 +1576,21 @@ "properties": { "average": { "type": "float" - }, - "max": { - "type": "float" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "timestamp": { - "type": "date" - } - } - }, - "source_node": { - "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" + }, + "max": { + "type": "float" + } + } }, "timestamp": { - "format": "date_time", "type": "date" }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } }, @@ -3159,63 +1627,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "kibana_stats": { "properties": { - "cloud": { - "properties": { - "id": { - "type": "keyword" - }, - "metadata": { - "type": "object" - }, - "name": { - "type": "keyword" - }, - "region": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "zone": { - "type": "keyword" - } - } - }, "concurrent_connections": { "type": "long" }, "kibana": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -3238,22 +1659,6 @@ "type": "half_float" } } - }, - "memory": { - "properties": { - "free_in_bytes": { - "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -3268,12 +1673,6 @@ "properties": { "size_limit": { "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" } } }, @@ -3281,9 +1680,6 @@ "type": "float" } } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -3292,9 +1688,6 @@ "disconnects": { "type": "long" }, - "status_codes": { - "type": "object" - }, "total": { "type": "long" } @@ -3310,49 +1703,15 @@ } } }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "timestamp": { - "type": "date" - } - } - }, - "source_node": { - "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "timestamp": { - "format": "date_time", "type": "date" }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } }, @@ -3389,79 +1748,27 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "logstash_state": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "pipeline": { "properties": { - "batch_size": { - "type": "integer" - }, - "ephemeral_id": { - "type": "keyword" - }, - "format": { - "type": "keyword" - }, "hash": { "type": "keyword" }, "id": { "type": "keyword" - }, - "representation": { - "enabled": false, - "type": "object" - }, - "version": { - "type": "keyword" - }, - "workers": { - "type": "short" } } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" } } }, "logstash_stats": { "properties": { - "batch_size": { - "type": "integer" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, "in": { "type": "long" }, @@ -3472,34 +1779,6 @@ }, "jvm": { "properties": { - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -3507,9 +1786,6 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } }, @@ -3520,34 +1796,6 @@ }, "logstash": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "batch_size": { - "type": "long" - }, - "workers": { - "type": "short" - } - } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -3562,9 +1810,6 @@ "properties": { "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -3582,9 +1827,6 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } @@ -3613,25 +1855,13 @@ }, "pipelines": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, - "in": { - "type": "long" - }, "out": { "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" } } }, @@ -3643,9 +1873,6 @@ }, "queue": { "properties": { - "events_count": { - "type": "long" - }, "max_queue_size_in_bytes": { "type": "long" }, @@ -3657,29 +1884,8 @@ } } }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, "vertices": { "properties": { - "double_gauges": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - }, - "type": "nested" - }, "duration_in_millis": { "type": "long" }, @@ -3692,22 +1898,11 @@ "id": { "type": "keyword" }, - "long_counters": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - }, - "type": "nested" - }, "pipeline_ephemeral_id": { "type": "keyword" }, "queue_push_duration_in_millis": { - "type": "long" + "type": "float" } }, "type": "nested" @@ -3723,12 +1918,6 @@ "type": "long" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -3736,50 +1925,24 @@ "properties": { "events_count": { "type": "long" - }, - "type": { - "type": "keyword" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" } } }, "timestamp": { "type": "date" - }, - "workers": { - "type": "short" } } }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/setup/collection/kibana_exclusive_mb/mappings.json b/x-pack/test/functional/es_archives/monitoring/setup/collection/kibana_exclusive_mb/mappings.json index a3ddceca893c0..4f388b8834b7b 100644 --- a/x-pack/test/functional/es_archives/monitoring/setup/collection/kibana_exclusive_mb/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/setup/collection/kibana_exclusive_mb/mappings.json @@ -9,105 +9,6 @@ "properties": { "beats_state": { "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "state": { - "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, "timestamp": { "format": "date_time", "type": "date" @@ -118,12 +19,6 @@ "properties": { "beat": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -139,146 +34,19 @@ "properties": { "apm-server": { "properties": { - "decoder": { - "properties": { - "deflate": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "gzip": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "missing-content-length": { - "properties": { - "count": { - "type": "long" - } - } - }, - "reader": { - "properties": { - "count": { - "type": "long" - }, - "size": { - "type": "long" - } - } - }, - "uncompressed": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - }, "processor": { "properties": { "error": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "errors": { - "type": "long" - }, - "frames": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } }, "metric": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - } - } - }, - "sourcemap": { - "properties": { - "counter": { - "type": "long" - }, - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } }, @@ -291,40 +59,8 @@ }, "transaction": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "frames": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "transactions": { - "type": "long" - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } } @@ -332,17 +68,6 @@ }, "server": { "properties": { - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "request": { "properties": { "count": { @@ -363,9 +88,6 @@ "concurrency": { "type": "long" }, - "count": { - "type": "long" - }, "decode": { "type": "long" }, @@ -400,9 +122,6 @@ "accepted": { "type": "long" }, - "count": { - "type": "long" - }, "ok": { "type": "long" } @@ -418,65 +137,17 @@ "properties": { "cpu": { "properties": { - "system": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "total": { "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - }, "value": { "type": "long" } } - }, - "user": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } } } }, "handles": { "properties": { - "limit": { - "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - }, "open": { "type": "long" } @@ -486,13 +157,6 @@ "properties": { "ephemeral_id": { "type": "keyword" - }, - "uptime": { - "properties": { - "ms": { - "type": "long" - } - } } } }, @@ -516,26 +180,6 @@ }, "libbeat": { "properties": { - "config": { - "properties": { - "module": { - "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { - "type": "long" - } - } - }, - "reloads": { - "type": "long" - } - } - }, "output": { "properties": { "events": { @@ -546,21 +190,12 @@ "active": { "type": "long" }, - "batches": { - "type": "long" - }, "dropped": { "type": "long" }, - "duplicates": { - "type": "long" - }, "failed": { "type": "long" }, - "toomany": { - "type": "long" - }, "total": { "type": "long" } @@ -576,9 +211,6 @@ } } }, - "type": { - "type": "keyword" - }, "write": { "properties": { "bytes": { @@ -593,23 +225,14 @@ }, "pipeline": { "properties": { - "clients": { - "type": "long" - }, "events": { "properties": { - "active": { - "type": "long" - }, "dropped": { "type": "long" }, "failed": { "type": "long" }, - "filtered": { - "type": "long" - }, "published": { "type": "long" }, @@ -620,13 +243,6 @@ "type": "long" } } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } } } } @@ -644,19 +260,6 @@ }, "5": { "type": "double" - }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "15": { - "type": "double" - }, - "5": { - "type": "double" - } - } } } } @@ -664,9 +267,6 @@ } } }, - "tags": { - "type": "keyword" - }, "timestamp": { "format": "date_time", "type": "date" @@ -676,25 +276,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -714,108 +305,29 @@ "format": "7", "number_of_replicas": "0", "number_of_shards": "1" - } - } - } -} - -{ - "type": "index", - "value": { - "aliases": { - }, - "index": ".monitoring-es-7-2019.04.09", - "mappings": { - "date_detection": false, - "dynamic": "false", - "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": ".monitoring-es-7-2019.04.09", + "mappings": { + "date_detection": false, + "dynamic": "false", + "properties": { "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -825,112 +337,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -945,16 +375,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -975,22 +395,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -998,66 +402,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -1072,18 +420,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1111,17 +449,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1134,17 +463,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1212,13 +532,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -1228,105 +541,37 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "interval_ms": { - "type": "long" - }, - "job_stats": { - "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, - "job_id": { - "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } } } - }, - "state": { + } + } + }, + "job_stats": { + "properties": { + "job_id": { "type": "keyword" } } @@ -1335,13 +580,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -1349,15 +587,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -1369,12 +601,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -1382,18 +608,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1414,33 +630,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1490,13 +688,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -1545,15 +736,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -1563,9 +748,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1583,26 +765,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -1610,14 +776,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -1633,12 +793,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -1651,22 +805,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1677,9 +815,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1690,22 +825,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1716,22 +835,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1760,12 +863,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1773,22 +870,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -1828,93 +912,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -1924,112 +929,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -2039,17 +962,7 @@ "properties": { "docs": { "properties": { - "count": { - "type": "long" - } - } - }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { + "count": { "type": "long" } } @@ -2074,22 +987,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -2097,66 +994,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -2171,18 +1012,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -2210,17 +1041,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -2233,17 +1055,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -2311,13 +1124,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -2327,33 +1133,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -2375,58 +1161,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -2434,13 +1172,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -2448,15 +1179,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -2468,12 +1193,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -2481,18 +1200,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -2513,33 +1222,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -2589,13 +1280,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -2644,15 +1328,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -2662,9 +1340,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -2682,26 +1357,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -2709,14 +1368,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -2732,12 +1385,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -2750,22 +1397,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2776,9 +1407,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2789,22 +1417,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -2814,23 +1426,7 @@ "type": "integer" }, "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" + "type": "long" } } }, @@ -2859,12 +1455,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -2872,22 +1462,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -2929,63 +1506,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "kibana_stats": { "properties": { - "cloud": { - "properties": { - "id": { - "type": "keyword" - }, - "metadata": { - "type": "object" - }, - "name": { - "type": "keyword" - }, - "region": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "zone": { - "type": "keyword" - } - } - }, "concurrent_connections": { "type": "long" }, "kibana": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -3008,22 +1538,6 @@ "type": "half_float" } } - }, - "memory": { - "properties": { - "free_in_bytes": { - "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -3038,12 +1552,6 @@ "properties": { "size_limit": { "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" } } }, @@ -3051,9 +1559,6 @@ "type": "float" } } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -3062,9 +1567,6 @@ "disconnects": { "type": "long" }, - "status_codes": { - "type": "object" - }, "total": { "type": "long" } @@ -3080,49 +1582,15 @@ } } }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "timestamp": { - "type": "date" - } - } - }, - "source_node": { - "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "timestamp": { - "format": "date_time", "type": "date" }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } }, @@ -3159,79 +1627,27 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "logstash_state": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "pipeline": { "properties": { - "batch_size": { - "type": "integer" - }, - "ephemeral_id": { - "type": "keyword" - }, - "format": { - "type": "keyword" - }, "hash": { "type": "keyword" }, "id": { "type": "keyword" - }, - "representation": { - "enabled": false, - "type": "object" - }, - "version": { - "type": "keyword" - }, - "workers": { - "type": "short" } } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" } } }, "logstash_stats": { "properties": { - "batch_size": { - "type": "integer" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, "in": { "type": "long" }, @@ -3242,34 +1658,6 @@ }, "jvm": { "properties": { - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -3277,9 +1665,6 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } }, @@ -3290,34 +1675,6 @@ }, "logstash": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "batch_size": { - "type": "long" - }, - "workers": { - "type": "short" - } - } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -3332,9 +1689,6 @@ "properties": { "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -3352,9 +1706,6 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } @@ -3383,25 +1734,13 @@ }, "pipelines": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, - "in": { - "type": "long" - }, "out": { "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" } } }, @@ -3413,9 +1752,6 @@ }, "queue": { "properties": { - "events_count": { - "type": "long" - }, "max_queue_size_in_bytes": { "type": "long" }, @@ -3427,29 +1763,8 @@ } } }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, "vertices": { "properties": { - "double_gauges": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - }, - "type": "nested" - }, "duration_in_millis": { "type": "long" }, @@ -3462,22 +1777,11 @@ "id": { "type": "keyword" }, - "long_counters": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - }, - "type": "nested" - }, "pipeline_ephemeral_id": { "type": "keyword" }, "queue_push_duration_in_millis": { - "type": "long" + "type": "float" } }, "type": "nested" @@ -3493,12 +1797,6 @@ "type": "long" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -3506,50 +1804,24 @@ "properties": { "events_count": { "type": "long" - }, - "type": { - "type": "keyword" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" } } }, "timestamp": { "type": "date" - }, - "workers": { - "type": "short" } } }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/setup/collection/kibana_mb/mappings.json b/x-pack/test/functional/es_archives/monitoring/setup/collection/kibana_mb/mappings.json index adbd44d7281b6..03f954f92dd33 100644 --- a/x-pack/test/functional/es_archives/monitoring/setup/collection/kibana_mb/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/setup/collection/kibana_mb/mappings.json @@ -9,105 +9,6 @@ "properties": { "beats_state": { "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "state": { - "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, "timestamp": { "format": "date_time", "type": "date" @@ -118,12 +19,6 @@ "properties": { "beat": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -139,146 +34,19 @@ "properties": { "apm-server": { "properties": { - "decoder": { - "properties": { - "deflate": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "gzip": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "missing-content-length": { - "properties": { - "count": { - "type": "long" - } - } - }, - "reader": { - "properties": { - "count": { - "type": "long" - }, - "size": { - "type": "long" - } - } - }, - "uncompressed": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - }, "processor": { "properties": { "error": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "errors": { - "type": "long" - }, - "frames": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } }, "metric": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - } - } - }, - "sourcemap": { - "properties": { - "counter": { - "type": "long" - }, - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } }, @@ -291,40 +59,8 @@ }, "transaction": { "properties": { - "decoding": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "frames": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "transactions": { - "type": "long" - }, "transformations": { "type": "long" - }, - "validation": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } } } } @@ -332,17 +68,6 @@ }, "server": { "properties": { - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "request": { "properties": { "count": { @@ -363,9 +88,6 @@ "concurrency": { "type": "long" }, - "count": { - "type": "long" - }, "decode": { "type": "long" }, @@ -400,9 +122,6 @@ "accepted": { "type": "long" }, - "count": { - "type": "long" - }, "ok": { "type": "long" } @@ -418,65 +137,17 @@ "properties": { "cpu": { "properties": { - "system": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "total": { "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - }, "value": { "type": "long" } } - }, - "user": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } } } }, "handles": { "properties": { - "limit": { - "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - }, "open": { "type": "long" } @@ -486,13 +157,6 @@ "properties": { "ephemeral_id": { "type": "keyword" - }, - "uptime": { - "properties": { - "ms": { - "type": "long" - } - } } } }, @@ -516,26 +180,6 @@ }, "libbeat": { "properties": { - "config": { - "properties": { - "module": { - "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { - "type": "long" - } - } - }, - "reloads": { - "type": "long" - } - } - }, "output": { "properties": { "events": { @@ -546,21 +190,12 @@ "active": { "type": "long" }, - "batches": { - "type": "long" - }, "dropped": { "type": "long" }, - "duplicates": { - "type": "long" - }, "failed": { "type": "long" }, - "toomany": { - "type": "long" - }, "total": { "type": "long" } @@ -576,9 +211,6 @@ } } }, - "type": { - "type": "keyword" - }, "write": { "properties": { "bytes": { @@ -592,24 +224,15 @@ } }, "pipeline": { - "properties": { - "clients": { - "type": "long" - }, + "properties": { "events": { "properties": { - "active": { - "type": "long" - }, "dropped": { "type": "long" }, "failed": { "type": "long" }, - "filtered": { - "type": "long" - }, "published": { "type": "long" }, @@ -620,13 +243,6 @@ "type": "long" } } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } } } } @@ -644,19 +260,6 @@ }, "5": { "type": "double" - }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "15": { - "type": "double" - }, - "5": { - "type": "double" - } - } } } } @@ -664,9 +267,6 @@ } } }, - "tags": { - "type": "keyword" - }, "timestamp": { "format": "date_time", "type": "date" @@ -676,25 +276,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -729,93 +320,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -825,112 +337,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -945,16 +375,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -975,22 +395,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -998,66 +402,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -1072,18 +420,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1111,17 +449,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1134,17 +463,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1209,32 +529,15 @@ "indices_stats": { "properties": { "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_time_in_millis": { - "type": "long" - }, - "index_total": { - "type": "long" - } - } - }, - "search": { + "properties": { + "primaries": { + "properties": { + "indexing": { "properties": { - "query_time_in_millis": { + "index_time_in_millis": { "type": "long" }, - "query_total": { + "index_total": { "type": "long" } } @@ -1243,18 +546,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -1276,58 +569,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -1335,13 +580,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -1349,15 +587,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -1369,12 +601,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -1382,18 +608,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -1414,33 +630,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1490,13 +688,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -1545,15 +736,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -1563,9 +748,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1583,26 +765,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -1610,14 +776,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -1633,12 +793,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -1651,22 +805,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1677,9 +815,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1690,22 +825,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1716,22 +835,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1760,12 +863,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1773,22 +870,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } @@ -1830,63 +914,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "kibana_stats": { "properties": { - "cloud": { - "properties": { - "id": { - "type": "keyword" - }, - "metadata": { - "type": "object" - }, - "name": { - "type": "keyword" - }, - "region": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "zone": { - "type": "keyword" - } - } - }, "concurrent_connections": { "type": "long" }, "kibana": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -1909,22 +946,6 @@ "type": "half_float" } } - }, - "memory": { - "properties": { - "free_in_bytes": { - "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -1939,12 +960,6 @@ "properties": { "size_limit": { "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" } } }, @@ -1952,9 +967,6 @@ "type": "float" } } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -1963,9 +975,6 @@ "disconnects": { "type": "long" }, - "status_codes": { - "type": "object" - }, "total": { "type": "long" } @@ -1975,55 +984,21 @@ "properties": { "average": { "type": "float" - }, - "max": { - "type": "float" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "timestamp": { - "type": "date" - } - } - }, - "source_node": { - "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" + }, + "max": { + "type": "float" + } + } }, "timestamp": { - "format": "date_time", "type": "date" }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } }, @@ -2060,63 +1035,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "kibana_stats": { "properties": { - "cloud": { - "properties": { - "id": { - "type": "keyword" - }, - "metadata": { - "type": "object" - }, - "name": { - "type": "keyword" - }, - "region": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "zone": { - "type": "keyword" - } - } - }, "concurrent_connections": { "type": "long" }, "kibana": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -2139,22 +1067,6 @@ "type": "half_float" } } - }, - "memory": { - "properties": { - "free_in_bytes": { - "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -2169,12 +1081,6 @@ "properties": { "size_limit": { "type": "float" - }, - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" } } }, @@ -2182,9 +1088,6 @@ "type": "float" } } - }, - "uptime_in_millis": { - "type": "long" } } }, @@ -2193,9 +1096,6 @@ "disconnects": { "type": "long" }, - "status_codes": { - "type": "object" - }, "total": { "type": "long" } @@ -2211,49 +1111,15 @@ } } }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "timestamp": { - "type": "date" - } - } - }, - "source_node": { - "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "timestamp": { - "format": "date_time", "type": "date" }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } }, @@ -2290,79 +1156,27 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, "logstash_state": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "pipeline": { "properties": { - "batch_size": { - "type": "integer" - }, - "ephemeral_id": { - "type": "keyword" - }, - "format": { - "type": "keyword" - }, "hash": { "type": "keyword" }, "id": { "type": "keyword" - }, - "representation": { - "enabled": false, - "type": "object" - }, - "version": { - "type": "keyword" - }, - "workers": { - "type": "short" } } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" } } }, "logstash_stats": { "properties": { - "batch_size": { - "type": "integer" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, "in": { "type": "long" }, @@ -2373,34 +1187,6 @@ }, "jvm": { "properties": { - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -2408,9 +1194,6 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } }, @@ -2421,34 +1204,6 @@ }, "logstash": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "batch_size": { - "type": "long" - }, - "workers": { - "type": "short" - } - } - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, "uuid": { "type": "keyword" }, @@ -2463,9 +1218,6 @@ "properties": { "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -2483,9 +1235,6 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } @@ -2514,25 +1263,13 @@ }, "pipelines": { "properties": { - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { "duration_in_millis": { "type": "long" }, - "filtered": { - "type": "long" - }, - "in": { - "type": "long" - }, "out": { "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" } } }, @@ -2544,9 +1281,6 @@ }, "queue": { "properties": { - "events_count": { - "type": "long" - }, "max_queue_size_in_bytes": { "type": "long" }, @@ -2558,29 +1292,8 @@ } } }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, "vertices": { "properties": { - "double_gauges": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - }, - "type": "nested" - }, "duration_in_millis": { "type": "long" }, @@ -2593,22 +1306,11 @@ "id": { "type": "keyword" }, - "long_counters": { - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - }, - "type": "nested" - }, "pipeline_ephemeral_id": { "type": "keyword" }, "queue_push_duration_in_millis": { - "type": "long" + "type": "float" } }, "type": "nested" @@ -2624,12 +1326,6 @@ "type": "long" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -2637,50 +1333,24 @@ "properties": { "events_count": { "type": "long" - }, - "type": { - "type": "keyword" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" } } }, "timestamp": { "type": "date" - }, - "workers": { - "type": "short" } } }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster-basic-beats/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster-basic-beats/mappings.json index 212391b038477..ba08a3d9f2460 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster-basic-beats/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster-basic-beats/mappings.json @@ -7,105 +7,6 @@ "properties": { "beats_state": { "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "state": { - "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, "timestamp": { "format": "date_time", "type": "date" @@ -116,12 +17,6 @@ "properties": { "beat": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -135,108 +30,40 @@ }, "metrics": { "properties": { - "beat": { + "apm-server": { "properties": { - "cpu": { + "processor": { "properties": { - "system": { + "error": { "properties": { - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } }, - "total": { + "metric": { "properties": { - "value": { - "type": "long" - }, - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } }, - "user": { + "span": { "properties": { - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } - } - } - }, - "info": { - "properties": { - "ephemeral_id": { - "type": "keyword" }, - "uptime": { + "transaction": { "properties": { - "ms": { + "transformations": { "type": "long" } } } } }, - "memstats": { - "properties": { - "gc_next": { - "type": "long" - }, - "memory_alloc": { - "type": "long" - }, - "memory_total": { - "type": "long" - }, - "rss": { - "type": "long" - } - } - }, - "handles": { - "properties": { - "open": { - "type": "long" - }, - "limit": { - "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - } - } - } - } - }, - "apm-server": { - "properties": { "server": { "properties": { "request": { @@ -246,17 +73,6 @@ } } }, - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "response": { "properties": { "count": { @@ -264,53 +80,47 @@ }, "errors": { "properties": { - "count": { - "type": "long" - }, - "toolarge": { + "closed": { "type": "long" }, - "validate": { + "concurrency": { "type": "long" }, - "ratelimit": { + "decode": { "type": "long" }, - "queue": { + "forbidden": { "type": "long" }, - "closed": { + "internal": { "type": "long" }, - "forbidden": { + "method": { "type": "long" }, - "concurrency": { + "queue": { "type": "long" }, - "unauthorized": { + "ratelimit": { "type": "long" }, - "internal": { + "toolarge": { "type": "long" }, - "decode": { + "unauthorized": { "type": "long" }, - "method": { + "validate": { "type": "long" } } }, "valid": { "properties": { - "ok": { - "type": "long" - }, "accepted": { "type": "long" }, - "count": { + "ok": { "type": "long" } } @@ -318,296 +128,109 @@ } } } - }, - "decoder": { + } + } + }, + "beat": { + "properties": { + "cpu": { "properties": { - "deflate": { + "total": { "properties": { - "content-length": { - "type": "long" - }, - "count": { + "value": { "type": "long" } } + } + } + }, + "handles": { + "properties": { + "open": { + "type": "long" + } + } + }, + "info": { + "properties": { + "ephemeral_id": { + "type": "keyword" + } + } + }, + "memstats": { + "properties": { + "gc_next": { + "type": "long" + }, + "memory_alloc": { + "type": "long" + }, + "memory_total": { + "type": "long" }, - "gzip": { + "rss": { + "type": "long" + } + } + } + } + }, + "libbeat": { + "properties": { + "output": { + "properties": { + "events": { "properties": { - "content-length": { + "acked": { "type": "long" }, - "count": { + "active": { "type": "long" - } - } - }, - "uncompressed": { - "properties": { - "content-length": { + }, + "dropped": { "type": "long" }, - "count": { + "failed": { + "type": "long" + }, + "total": { "type": "long" } } }, - "reader": { + "read": { "properties": { - "size": { + "bytes": { "type": "long" }, - "count": { + "errors": { "type": "long" } } }, - "missing-content-length": { + "write": { "properties": { - "count": { + "bytes": { + "type": "long" + }, + "errors": { "type": "long" } } } } }, - "processor": { - "properties": { - "metric": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - } - } - }, - "sourcemap": { - "properties": { - "counter": { - "type": "long" - }, - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - }, - "transaction": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "transactions": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "error": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "errors": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "span": { - "properties": { - "transformations": { - "type": "long" - } - } - } - } - } - } - }, - "libbeat": { - "properties": { - "config": { - "properties": { - "module": { - "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { - "type": "long" - } - } - }, - "reloads": { - "type": "long" - } - } - }, - "output": { + "pipeline": { "properties": { "events": { "properties": { - "acked": { - "type": "long" - }, - "active": { - "type": "long" - }, - "batches": { - "type": "long" - }, "dropped": { "type": "long" }, - "duplicates": { - "type": "long" - }, "failed": { "type": "long" }, - "total": { - "type": "long" - }, - "toomany": { - "type": "long" - } - } - }, - "read": { - "properties": { - "bytes": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "write": { - "properties": { - "bytes": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - } - } - }, - "pipeline": { - "properties": { - "clients": { - "type": "long" - }, - "events": { - "properties": { - "active": { - "type": "long" - }, - "dropped": { - "type": "long" - }, - "failed": { - "type": "long" - }, - "filtered": { - "type": "long" - }, "published": { "type": "long" }, @@ -618,13 +241,6 @@ "type": "long" } } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } } } } @@ -637,24 +253,11 @@ "1": { "type": "double" }, - "5": { - "type": "double" - }, "15": { "type": "double" }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "5": { - "type": "double" - }, - "15": { - "type": "double" - } - } + "5": { + "type": "double" } } } @@ -662,9 +265,6 @@ } } }, - "tags": { - "type": "keyword" - }, "timestamp": { "format": "date_time", "type": "date" @@ -674,25 +274,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -727,95 +318,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -823,86 +340,40 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { - "memory": { - "properties": { - "heap": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, - "size_limit": { - "type": "float" - } - } - }, - "resident_set_size_in_bytes": { - "type": "float" - } - } - }, "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } + "type": "float" }, - "https": { + "memory": { "properties": { - "total": { - "type": "long" + "heap": { + "properties": { + "size_limit": { + "type": "float" + } + } + }, + "resident_set_size_in_bytes": { + "type": "float" } } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -910,9 +381,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -926,10 +394,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -953,115 +435,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -1076,29 +490,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -1113,44 +510,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -1159,42 +521,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -1203,36 +535,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -1251,14 +566,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -1267,24 +580,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -1294,41 +598,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -1337,68 +641,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -1419,15 +747,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1435,15 +754,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1462,116 +772,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1587,16 +878,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -1608,12 +893,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -1623,12 +902,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -1638,71 +911,10 @@ } } }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, "thread_pool": { "properties": { "bulk": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1713,9 +925,6 @@ }, "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1726,22 +935,6 @@ }, "index": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1752,22 +945,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1790,246 +967,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster-green-gold/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster-green-gold/mappings.json index bd411ee5178e2..702f8731a31ca 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster-green-gold/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster-green-gold/mappings.json @@ -1,7 +1,8 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-kibana-6-2017.08.23", "mappings": { "dynamic": false, @@ -9,95 +10,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -105,47 +32,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -155,36 +63,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -192,9 +73,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -208,10 +86,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -230,29 +122,15 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-alerts-6", "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -274,6 +152,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, @@ -292,7 +185,8 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-logstash-6-2017.08.23", "mappings": { "dynamic": false, @@ -300,83 +194,25 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, - "logstash_stats": { - "type": "object", + "logstash_state": { "properties": { - "logstash": { + "pipeline": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "version": { + "hash": { "type": "keyword" }, - "snapshot": { - "type": "boolean" - }, - "status": { + "id": { "type": "keyword" - }, - "pipeline": { - "properties": { - "workers": { - "type": "short" - }, - "batch_size": { - "type": "long" - } - } } } - }, + } + } + }, + "logstash_stats": { + "properties": { "events": { "properties": { - "filtered": { + "duration_in_millis": { "type": "long" }, "in": { @@ -384,210 +220,120 @@ }, "out": { "type": "long" - }, - "duration_in_millis": { - "type": "long" } } }, - "timestamp": { - "type": "date" - }, "jvm": { "properties": { + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + } + } + }, "uptime_in_millis": { "type": "long" + } + } + }, + "logstash": { + "properties": { + "uuid": { + "type": "keyword" }, - "gc": { + "version": { + "type": "keyword" + } + } + }, + "os": { + "properties": { + "cgroup": { "properties": { - "collectors": { + "cpu": { "properties": { - "old": { + "stat": { "properties": { - "collection_count": { + "number_of_elapsed_periods": { "type": "long" }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { + "number_of_times_throttled": { "type": "long" }, - "collection_time_in_millis": { + "time_throttled_nanos": { "type": "long" } } } } - } - } - }, - "mem": { - "properties": { - "heap_max_in_bytes": { - "type": "long" - }, - "heap_used_in_bytes": { - "type": "long" }, - "heap_used_percent": { - "type": "long" + "cpuacct": { + "properties": { + "usage_nanos": { + "type": "long" + } + } } } - } - } - }, - "os": { - "properties": { + }, "cpu": { "properties": { "load_average": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } } } - }, - "cgroup": { - "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, - "cpu": { - "properties": { - "control_group": { - "type": "keyword" - }, - "stat": { - "properties": { - "number_of_elapsed_periods": { - "type": "long" - }, - "number_of_times_throttled": { - "type": "long" - }, - "time_throttled_nanos": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "process": { - "properties": { - "cpu": { - "properties": { - "percent": { - "type": "long" - } - } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" } } }, "pipelines": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { - "in": { - "type": "long" - }, - "filtered": { - "type": "long" - }, - "out": { - "type": "long" - }, "duration_in_millis": { "type": "long" }, - "queue_push_duration_in_millis": { + "out": { "type": "long" } } }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, "queue": { "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" - }, "max_queue_size_in_bytes": { "type": "long" }, "queue_size_in_bytes": { "type": "long" + }, + "type": { + "type": "keyword" } } }, "vertices": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "pipeline_ephemeral_id": { - "type": "keyword" + "duration_in_millis": { + "type": "long" }, "events_in": { "type": "long" @@ -595,111 +341,63 @@ "events_out": { "type": "long" }, - "duration_in_millis": { - "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" + "id": { + "type": "keyword" }, - "long_counters": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - } + "pipeline_ephemeral_id": { + "type": "keyword" }, - "double_gauges": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - } + "queue_push_duration_in_millis": { + "type": "float" } - } - }, - "reloads": { + }, + "type": "nested" + } + }, + "type": "nested" + }, + "process": { + "properties": { + "cpu": { "properties": { - "failures": { - "type": "long" - }, - "successes": { + "percent": { "type": "long" } } } } }, - "workers": { - "type": "short" + "queue": { + "properties": { + "events_count": { + "type": "long" + } + } }, - "batch_size": { - "type": "integer" + "timestamp": { + "type": "date" } } }, - "logstash_state": { + "metricset": { "properties": { - "uuid": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "workers": { - "type": "short" - }, - "batch_size": { - "type": "integer" - }, - "format": { - "type": "keyword" - }, - "version": { + "fields": { + "keyword": { + "ignore_above": 256, "type": "keyword" - }, - "representation": { - "enabled": false } - } + }, + "type": "text" } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -718,121 +416,54 @@ { "type": "index", "value": { - "aliases": {}, + "aliases": { + }, "index": ".monitoring-es-6-2017.08.23", "mappings": { "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" + } + } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -847,29 +478,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -884,44 +498,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -930,42 +509,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -974,36 +523,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -1022,14 +554,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -1038,24 +568,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -1065,41 +586,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -1108,68 +629,92 @@ } } }, - "cluster_stats": { - "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" - } - } - }, - "cluster_state": { - "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" - } - } - }, - "node_stats": { + "indices_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "_all": { "properties": { - "docs": { + "primaries": { "properties": { - "count": { - "type": "long" + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "job_stats": { + "properties": { + "job_id": { + "type": "keyword" + } + } + }, + "node_stats": { + "properties": { + "fs": { + "properties": { + "io_stats": { + "properties": { + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -1190,15 +735,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1206,15 +742,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1233,116 +760,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1358,16 +866,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -1379,12 +881,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -1394,12 +890,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -1409,71 +899,10 @@ } } }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, "thread_pool": { "properties": { "bulk": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1484,9 +913,6 @@ }, "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1497,22 +923,6 @@ }, "index": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1523,22 +933,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1561,246 +955,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster-green-platinum/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster-green-platinum/mappings.json index 0cae1b02ccaca..640b6f07f3470 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster-green-platinum/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster-green-platinum/mappings.json @@ -8,95 +8,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -104,47 +30,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -154,36 +61,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -191,9 +71,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -207,10 +84,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -235,105 +126,6 @@ "properties": { "beats_state": { "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "state": { - "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, "timestamp": { "format": "date_time", "type": "date" @@ -344,12 +136,6 @@ "properties": { "beat": { "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -363,128 +149,49 @@ }, "metrics": { "properties": { - "beat": { + "apm-server": { "properties": { - "cpu": { + "processor": { "properties": { - "system": { + "error": { "properties": { - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } }, - "total": { + "metric": { "properties": { - "value": { - "type": "long" - }, - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } }, - "user": { + "span": { "properties": { - "ticks": { + "transformations": { "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } } } - } - } - }, - "info": { - "properties": { - "ephemeral_id": { - "type": "keyword" }, - "uptime": { + "transaction": { "properties": { - "ms": { + "transformations": { "type": "long" } } } } }, - "memstats": { - "properties": { - "gc_next": { - "type": "long" - }, - "memory_alloc": { - "type": "long" - }, - "memory_total": { - "type": "long" - }, - "rss": { - "type": "long" - } - } - }, - "handles": { + "server": { "properties": { - "open": { - "type": "long" - }, - "limit": { + "request": { "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - } - } - } - } - }, - "apm-server": { - "properties": { - "server": { - "properties": { - "request": { - "properties": { - "count": { + "count": { "type": "long" } } }, - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, "response": { "properties": { "count": { @@ -492,53 +199,47 @@ }, "errors": { "properties": { - "count": { - "type": "long" - }, - "toolarge": { + "closed": { "type": "long" }, - "validate": { + "concurrency": { "type": "long" }, - "ratelimit": { + "decode": { "type": "long" }, - "queue": { + "forbidden": { "type": "long" }, - "closed": { + "internal": { "type": "long" }, - "forbidden": { + "method": { "type": "long" }, - "concurrency": { + "queue": { "type": "long" }, - "unauthorized": { + "ratelimit": { "type": "long" }, - "internal": { + "toolarge": { "type": "long" }, - "decode": { + "unauthorized": { "type": "long" }, - "method": { + "validate": { "type": "long" } } }, "valid": { "properties": { - "ok": { - "type": "long" - }, "accepted": { "type": "long" }, - "count": { + "ok": { "type": "long" } } @@ -546,195 +247,49 @@ } } } - }, - "decoder": { + } + } + }, + "beat": { + "properties": { + "cpu": { "properties": { - "deflate": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "gzip": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "uncompressed": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "reader": { - "properties": { - "size": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "missing-content-length": { + "total": { "properties": { - "count": { + "value": { "type": "long" } } } } }, - "processor": { + "handles": { "properties": { - "metric": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - } - } - }, - "sourcemap": { - "properties": { - "counter": { - "type": "long" - }, - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } + "open": { + "type": "long" + } + } + }, + "info": { + "properties": { + "ephemeral_id": { + "type": "keyword" + } + } + }, + "memstats": { + "properties": { + "gc_next": { + "type": "long" }, - "transaction": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "transactions": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } + "memory_alloc": { + "type": "long" }, - "error": { - "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "errors": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } + "memory_total": { + "type": "long" }, - "span": { - "properties": { - "transformations": { - "type": "long" - } - } + "rss": { + "type": "long" } } } @@ -742,26 +297,6 @@ }, "libbeat": { "properties": { - "config": { - "properties": { - "module": { - "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { - "type": "long" - } - } - }, - "reloads": { - "type": "long" - } - } - }, "output": { "properties": { "events": { @@ -772,23 +307,14 @@ "active": { "type": "long" }, - "batches": { - "type": "long" - }, "dropped": { "type": "long" }, - "duplicates": { - "type": "long" - }, "failed": { "type": "long" }, "total": { "type": "long" - }, - "toomany": { - "type": "long" } } }, @@ -802,9 +328,6 @@ } } }, - "type": { - "type": "keyword" - }, "write": { "properties": { "bytes": { @@ -819,23 +342,14 @@ }, "pipeline": { "properties": { - "clients": { - "type": "long" - }, "events": { "properties": { - "active": { - "type": "long" - }, "dropped": { "type": "long" }, "failed": { "type": "long" }, - "filtered": { - "type": "long" - }, "published": { "type": "long" }, @@ -846,13 +360,6 @@ "type": "long" } } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } } } } @@ -865,24 +372,11 @@ "1": { "type": "double" }, - "5": { - "type": "double" - }, "15": { "type": "double" }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "5": { - "type": "double" - }, - "15": { - "type": "double" - } - } + "5": { + "type": "double" } } } @@ -890,9 +384,6 @@ } } }, - "tags": { - "type": "keyword" - }, "timestamp": { "format": "date_time", "type": "date" @@ -902,25 +393,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -953,115 +435,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -1076,29 +490,64 @@ } } }, - "fielddata": { + "indexing": { "properties": { - "memory_size_in_bytes": { + "index_time_in_millis": { "type": "long" }, - "evictions": { + "index_total": { + "type": "long" + }, + "throttle_time_in_millis": { + "type": "long" + } + } + }, + "merges": { + "properties": { + "total_size_in_bytes": { + "type": "long" + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } }, + "segments": { + "properties": { + "count": { + "type": "integer" + } + } + }, "store": { "properties": { "size_in_bytes": { "type": "long" } } + } + } + }, + "total": { + "properties": { + "fielddata": { + "properties": { + "memory_size_in_bytes": { + "type": "long" + } + } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -1117,14 +566,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -1133,24 +580,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -1160,176 +598,85 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } } } - }, - "total": { + } + } + }, + "indices_stats": { + "properties": { + "_all": { "properties": { - "docs": { + "primaries": { "properties": { - "count": { - "type": "long" + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } } } }, - "fielddata": { + "total": { "properties": { - "memory_size_in_bytes": { - "type": "long" + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - }, - "throttle_time_in_millis": { - "type": "long" - } - } - }, - "merges": { - "properties": { - "total_size_in_bytes": { - "type": "long" - } - } - }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - }, - "segments": { - "properties": { - "count": { - "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - } - } - }, - "refresh": { - "properties": { - "total_time_in_millis": { - "type": "long" + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } } } } @@ -1337,68 +684,49 @@ } } }, - "cluster_stats": { - "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" - } - } - }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -1419,15 +747,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1435,15 +754,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -1462,116 +772,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -1587,95 +878,21 @@ } } }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } - } - } - }, - "cpu": { - "properties": { - "load_average": { + "cpuacct": { "properties": { - "1m": { - "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" + "usage_nanos": { + "type": "long" } - } - } - } - } - } - }, - "process": { - "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, - "cpu": { - "properties": { - "percent": { - "type": "half_float" - } - } - } - } - }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } + } + } + } + }, + "cpu": { + "properties": { + "load_average": { + "properties": { + "1m": { + "type": "half_float" } } } @@ -1683,26 +900,21 @@ } } }, - "thread_pool": { + "process": { "properties": { - "bulk": { + "cpu": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" + "percent": { + "type": "half_float" } } - }, - "generic": { + } + } + }, + "thread_pool": { + "properties": { + "bulk": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1713,9 +925,6 @@ }, "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1726,22 +935,6 @@ }, "index": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1752,22 +945,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1790,246 +967,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -2052,24 +1024,9 @@ "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -2091,6 +1048,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster-green-trial-two-nodes-one-cgrouped/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster-green-trial-two-nodes-one-cgrouped/mappings.json index babf4398a8502..6e6847a5fe30b 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster-green-trial-two-nodes-one-cgrouped/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster-green-trial-two-nodes-one-cgrouped/mappings.json @@ -8,95 +8,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -104,47 +30,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -154,36 +61,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -191,9 +71,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -207,10 +84,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -234,115 +125,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -357,29 +180,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -394,44 +200,9 @@ } } }, - "query_cache": { + "refresh": { "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -440,42 +211,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -484,36 +225,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -532,14 +256,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -548,24 +270,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -575,41 +288,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -618,68 +331,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -700,15 +437,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -716,15 +444,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -743,116 +462,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -868,16 +568,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -889,12 +583,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -904,12 +592,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -919,71 +601,10 @@ } } }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, "thread_pool": { "properties": { "bulk": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -994,9 +615,6 @@ }, "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1007,22 +625,6 @@ }, "index": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1033,22 +635,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1071,246 +657,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster-red-platinum/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster-red-platinum/mappings.json index b835ca4fdaf65..115d2fff4a4ac 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster-red-platinum/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster-red-platinum/mappings.json @@ -6,115 +6,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -129,29 +61,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -166,44 +81,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -212,42 +92,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -256,36 +106,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -304,14 +137,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -320,24 +151,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -347,41 +169,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -390,68 +212,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -472,15 +318,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -488,15 +325,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -515,116 +343,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -640,16 +449,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -661,12 +464,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -676,413 +473,106 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { "type": "half_float" } - } - } - } - }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "thread_pool": { - "properties": { - "bulk": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "get": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "index": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "search": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "write": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - } - } - } - } - }, - "index_recovery": { - "type": "object" - }, - "shard": { - "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, - "index": { - "type": "keyword" - }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, - "node": { - "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" - }, - "state": { - "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - } - } - }, - "ccr_stats": { - "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { - "type": "keyword" - }, - "follower_index": { - "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" + } + } + } }, - "read_exceptions": { - "type": "nested", + "thread_pool": { "properties": { - "from_seq_no": { - "type": "long" + "bulk": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } }, - "retries": { - "type": "integer" + "get": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } }, - "exception": { - "type": "object", + "index": { "properties": { - "type": { - "type": "keyword" + "queue": { + "type": "integer" }, - "reason": { - "type": "text" + "rejected": { + "type": "long" + } + } + }, + "search": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" } } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" }, - "reason": { - "type": "text" + "write": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } } } } } }, - "ccr_auto_follow_stats": { + "shard": { "properties": { - "number_of_failed_follow_indices": { - "type": "long" + "index": { + "type": "keyword" }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" + "node": { + "type": "keyword" }, - "number_of_successful_follow_indices": { - "type": "long" + "primary": { + "type": "boolean" }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } + "state": { + "type": "keyword" + } + } + }, + "source_node": { + "properties": { + "name": { + "type": "keyword" }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } + "uuid": { + "type": "keyword" } } + }, + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1107,95 +597,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -1203,47 +619,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -1253,36 +650,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -1290,9 +660,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -1306,10 +673,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1331,24 +712,9 @@ "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -1370,6 +736,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster-three-nodes-shard-relocation/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster-three-nodes-shard-relocation/mappings.json index d1101b9fb4659..f8edb978c0add 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster-three-nodes-shard-relocation/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster-three-nodes-shard-relocation/mappings.json @@ -8,95 +8,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -104,47 +30,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -154,36 +61,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -191,9 +71,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -207,10 +84,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -232,24 +123,9 @@ "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -271,6 +147,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, @@ -295,83 +186,25 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, - "logstash_stats": { - "type": "object", + "logstash_state": { "properties": { - "logstash": { + "pipeline": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "version": { + "hash": { "type": "keyword" }, - "snapshot": { - "type": "boolean" - }, - "status": { + "id": { "type": "keyword" - }, - "pipeline": { - "properties": { - "workers": { - "type": "short" - }, - "batch_size": { - "type": "long" - } - } } } - }, + } + } + }, + "logstash_stats": { + "properties": { "events": { "properties": { - "filtered": { + "duration_in_millis": { "type": "long" }, "in": { @@ -379,48 +212,11 @@ }, "out": { "type": "long" - }, - "duration_in_millis": { - "type": "long" } } }, - "timestamp": { - "type": "date" - }, "jvm": { "properties": { - "uptime_in_millis": { - "type": "long" - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, "mem": { "properties": { "heap_max_in_bytes": { @@ -428,50 +224,30 @@ }, "heap_used_in_bytes": { "type": "long" - }, - "heap_used_percent": { - "type": "long" } } - } - } + }, + "uptime_in_millis": { + "type": "long" + } + } }, - "os": { + "logstash": { "properties": { - "cpu": { - "properties": { - "load_average": { - "properties": { - "1m": { - "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" - } - } - } - } + "uuid": { + "type": "keyword" }, + "version": { + "type": "keyword" + } + } + }, + "os": { + "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -486,103 +262,70 @@ } } } + }, + "cpuacct": { + "properties": { + "usage_nanos": { + "type": "long" + } + } } } - } - } - }, - "process": { - "properties": { + }, "cpu": { "properties": { - "percent": { - "type": "long" + "load_average": { + "properties": { + "15m": { + "type": "half_float" + }, + "1m": { + "type": "half_float" + }, + "5m": { + "type": "half_float" + } + } } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" - } - } - }, - "reloads": { - "properties": { - "failures": { - "type": "long" - }, - "successes": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" } } }, "pipelines": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, "events": { "properties": { - "in": { - "type": "long" - }, - "filtered": { - "type": "long" - }, - "out": { - "type": "long" - }, "duration_in_millis": { "type": "long" }, - "queue_push_duration_in_millis": { + "out": { "type": "long" } } }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, "queue": { "properties": { - "events_count": { - "type": "long" - }, - "type": { - "type": "keyword" - }, "max_queue_size_in_bytes": { "type": "long" }, "queue_size_in_bytes": { "type": "long" + }, + "type": { + "type": "keyword" } } }, "vertices": { - "type": "nested", "properties": { - "id": { - "type": "keyword" - }, - "pipeline_ephemeral_id": { - "type": "keyword" + "duration_in_millis": { + "type": "long" }, "events_in": { "type": "long" @@ -590,111 +333,63 @@ "events_out": { "type": "long" }, - "duration_in_millis": { - "type": "long" - }, - "queue_push_duration_in_millis": { - "type": "long" + "id": { + "type": "keyword" }, - "long_counters": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "long" - } - } + "pipeline_ephemeral_id": { + "type": "keyword" }, - "double_gauges": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "value": { - "type": "double" - } - } + "queue_push_duration_in_millis": { + "type": "float" } - } - }, - "reloads": { + }, + "type": "nested" + } + }, + "type": "nested" + }, + "process": { + "properties": { + "cpu": { "properties": { - "failures": { - "type": "long" - }, - "successes": { + "percent": { "type": "long" } } } } }, - "workers": { - "type": "short" + "queue": { + "properties": { + "events_count": { + "type": "long" + } + } }, - "batch_size": { - "type": "integer" + "timestamp": { + "type": "date" } } }, - "logstash_state": { + "metricset": { "properties": { - "uuid": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "http_address": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, - "status": { - "type": "keyword" - }, - "pipeline": { - "properties": { - "id": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "workers": { - "type": "short" - }, - "batch_size": { - "type": "integer" - }, - "format": { - "type": "keyword" - }, - "version": { + "fields": { + "keyword": { + "ignore_above": 256, "type": "keyword" - }, - "representation": { - "enabled": false } - } + }, + "type": "text" } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -717,115 +412,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -840,29 +467,64 @@ } } }, - "fielddata": { + "indexing": { "properties": { - "memory_size_in_bytes": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { "type": "long" }, - "evictions": { + "throttle_time_in_millis": { + "type": "long" + } + } + }, + "merges": { + "properties": { + "total_size_in_bytes": { "type": "long" } } }, + "refresh": { + "properties": { + "total_time_in_millis": { + "type": "long" + } + } + }, + "segments": { + "properties": { + "count": { + "type": "integer" + } + } + }, "store": { "properties": { "size_in_bytes": { "type": "long" } } + } + } + }, + "total": { + "properties": { + "fielddata": { + "properties": { + "memory_size_in_bytes": { + "type": "long" + } + } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -881,14 +543,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -897,24 +557,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -924,79 +575,144 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } } } - }, - "total": { + } + } + }, + "indices_stats": { + "properties": { + "_all": { "properties": { - "docs": { + "primaries": { "properties": { - "count": { - "type": "long" + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } } } }, - "fielddata": { + "total": { "properties": { - "memory_size_in_bytes": { - "type": "long" + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } }, - "evictions": { - "type": "long" + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "job_stats": { + "properties": { + "job_id": { + "type": "keyword" + } + } + }, + "node_stats": { + "properties": { + "fs": { + "properties": { + "io_stats": { + "properties": { + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, - "store": { + "total": { "properties": { - "size_in_bytes": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { + "fielddata": { + "properties": { + "memory_size_in_bytes": { "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -1004,51 +720,26 @@ } } }, - "merges": { + "query_cache": { "properties": { - "total_size_in_bytes": { + "memory_size_in_bytes": { "type": "long" } } }, - "query_cache": { + "request_cache": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, - "request_cache": { + "search": { "properties": { - "memory_size_in_bytes": { + "query_time_in_millis": { "type": "long" }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -1058,178 +749,19 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, "doc_values_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, "fixed_bit_set_memory_in_bytes": { "type": "long" - } - } - }, - "refresh": { - "properties": { - "total_time_in_millis": { - "type": "long" - } - } - } - } - } - } - }, - "cluster_stats": { - "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" - } - } - }, - "cluster_state": { - "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" - } - } - }, - "node_stats": { - "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" }, - "evictions": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_time_in_millis": { - "type": "long" - }, - "index_total": { - "type": "long" - }, - "throttle_time_in_millis": { - "type": "long" - } - } - }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { + "index_writer_memory_in_bytes": { "type": "long" - } - } - }, - "segments": { - "properties": { - "count": { - "type": "integer" }, "memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, "points_memory_in_bytes": { @@ -1241,232 +773,125 @@ "term_vectors_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, "version_map_memory_in_bytes": { "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } } } }, - "fs": { - "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { - "properties": { - "total": { - "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" - }, - "write_kilobytes": { - "type": "long" - } - } - } - } - } - } - }, - "os": { + "jvm": { "properties": { - "cgroup": { + "gc": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, - "cpu": { + "collectors": { "properties": { - "cfs_quota_micros": { - "type": "long" - }, - "control_group": { - "type": "keyword" - }, - "stat": { + "old": { "properties": { - "number_of_elapsed_periods": { + "collection_count": { "type": "long" }, - "number_of_times_throttled": { + "collection_time_in_millis": { + "type": "long" + } + } + }, + "young": { + "properties": { + "collection_count": { "type": "long" }, - "time_throttled_nanos": { + "collection_time_in_millis": { "type": "long" } } } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } - } - } - }, - "cpu": { - "properties": { - "load_average": { - "properties": { - "1m": { - "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" - } - } } } - } - } - }, - "process": { - "properties": { - "open_file_descriptors": { - "type": "long" }, - "max_file_descriptors": { - "type": "long" - }, - "cpu": { - "properties": { - "percent": { - "type": "half_float" - } - } - } - } - }, - "jvm": { - "properties": { "mem": { "properties": { + "heap_max_in_bytes": { + "type": "long" + }, "heap_used_in_bytes": { "type": "long" }, "heap_used_percent": { "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" } } - }, - "gc": { + } + } + }, + "node_id": { + "type": "keyword" + }, + "os": { + "properties": { + "cgroup": { "properties": { - "collectors": { + "cpu": { "properties": { - "young": { + "cfs_quota_micros": { + "type": "long" + }, + "stat": { "properties": { - "collection_count": { + "number_of_elapsed_periods": { "type": "long" }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { + "number_of_times_throttled": { "type": "long" }, - "collection_time_in_millis": { + "time_throttled_nanos": { "type": "long" } } } } + }, + "cpuacct": { + "properties": { + "usage_nanos": { + "type": "long" + } + } + } + } + }, + "cpu": { + "properties": { + "load_average": { + "properties": { + "1m": { + "type": "half_float" + } + } } } } } }, - "thread_pool": { + "process": { "properties": { - "bulk": { + "cpu": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" + "percent": { + "type": "half_float" } } - }, - "generic": { + } + } + }, + "thread_pool": { + "properties": { + "bulk": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1477,9 +902,6 @@ }, "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1490,22 +912,6 @@ }, "index": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1516,22 +922,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -1554,246 +944,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-basic/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-basic/mappings.json index ce8427c5cd657..0715e68298b16 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-basic/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-basic/mappings.json @@ -6,115 +6,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -129,29 +61,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -166,44 +81,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -212,42 +92,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -256,36 +106,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -304,14 +137,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -320,24 +151,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -347,41 +169,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -390,68 +212,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -472,15 +318,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -488,15 +325,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -515,116 +343,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -640,16 +449,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -661,12 +464,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -676,12 +473,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -691,71 +482,10 @@ } } }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, "thread_pool": { "properties": { "bulk": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -766,9 +496,6 @@ }, "get": { "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -779,22 +506,6 @@ }, "index": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -805,22 +516,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -843,246 +538,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1104,24 +594,9 @@ "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -1143,6 +618,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-platinum--with-10-alerts/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-platinum--with-10-alerts/mappings.json index 07ba8e2f5b814..c1552898816b7 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-platinum--with-10-alerts/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-platinum--with-10-alerts/mappings.json @@ -6,115 +6,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -129,29 +61,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -166,44 +81,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -212,42 +92,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -256,36 +106,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -304,14 +137,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -320,24 +151,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -347,41 +169,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -390,68 +212,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -472,15 +318,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -488,15 +325,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -515,116 +343,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -640,16 +449,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -661,12 +464,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -676,413 +473,106 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { "type": "half_float" } - } - } - } - }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "thread_pool": { - "properties": { - "bulk": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "get": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "index": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "search": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "write": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - } - } - } - } - }, - "index_recovery": { - "type": "object" - }, - "shard": { - "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, - "index": { - "type": "keyword" - }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, - "node": { - "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" - }, - "state": { - "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - } - } - }, - "ccr_stats": { - "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { - "type": "keyword" - }, - "follower_index": { - "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" + } + } + } }, - "read_exceptions": { - "type": "nested", + "thread_pool": { "properties": { - "from_seq_no": { - "type": "long" + "bulk": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } }, - "retries": { - "type": "integer" + "get": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } }, - "exception": { - "type": "object", + "index": { "properties": { - "type": { - "type": "keyword" + "queue": { + "type": "integer" }, - "reason": { - "type": "text" + "rejected": { + "type": "long" + } + } + }, + "search": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" } } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" }, - "reason": { - "type": "text" + "write": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } } } } } }, - "ccr_auto_follow_stats": { + "shard": { "properties": { - "number_of_failed_follow_indices": { - "type": "long" + "index": { + "type": "keyword" }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" + "node": { + "type": "keyword" }, - "number_of_successful_follow_indices": { - "type": "long" + "primary": { + "type": "boolean" }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } + "state": { + "type": "keyword" + } + } + }, + "source_node": { + "properties": { + "name": { + "type": "keyword" }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } + "uuid": { + "type": "keyword" } } + }, + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1104,24 +594,9 @@ "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -1143,6 +618,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, @@ -1167,95 +657,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -1263,47 +679,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -1313,36 +710,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -1350,9 +720,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -1366,10 +733,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-platinum/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-platinum/mappings.json index 07ba8e2f5b814..c1552898816b7 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-platinum/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster-yellow-platinum/mappings.json @@ -6,115 +6,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -129,29 +61,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -166,44 +81,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -212,42 +92,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -256,36 +106,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -304,14 +137,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -320,24 +151,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -347,41 +169,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -390,68 +212,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -472,15 +318,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -488,15 +325,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -515,116 +343,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -640,16 +449,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -661,12 +464,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -676,413 +473,106 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { "type": "half_float" } - } - } - } - }, - "jvm": { - "properties": { - "mem": { - "properties": { - "heap_used_in_bytes": { - "type": "long" - }, - "heap_used_percent": { - "type": "half_float" - }, - "heap_max_in_bytes": { - "type": "long" - } - } - }, - "gc": { - "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "thread_pool": { - "properties": { - "bulk": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "get": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "index": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "search": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "write": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - } - } - } - } - }, - "index_recovery": { - "type": "object" - }, - "shard": { - "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, - "index": { - "type": "keyword" - }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, - "node": { - "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" - }, - "state": { - "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - } - } - }, - "ccr_stats": { - "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { - "type": "keyword" - }, - "follower_index": { - "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" + } + } + } }, - "read_exceptions": { - "type": "nested", + "thread_pool": { "properties": { - "from_seq_no": { - "type": "long" + "bulk": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } }, - "retries": { - "type": "integer" + "get": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } }, - "exception": { - "type": "object", + "index": { "properties": { - "type": { - "type": "keyword" + "queue": { + "type": "integer" }, - "reason": { - "type": "text" + "rejected": { + "type": "long" + } + } + }, + "search": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" } } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" }, - "reason": { - "type": "text" + "write": { + "properties": { + "queue": { + "type": "integer" + }, + "rejected": { + "type": "long" + } + } } } } } }, - "ccr_auto_follow_stats": { + "shard": { "properties": { - "number_of_failed_follow_indices": { - "type": "long" + "index": { + "type": "keyword" }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" + "node": { + "type": "keyword" }, - "number_of_successful_follow_indices": { - "type": "long" + "primary": { + "type": "boolean" }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } + "state": { + "type": "keyword" + } + } + }, + "source_node": { + "properties": { + "name": { + "type": "keyword" }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } + "uuid": { + "type": "keyword" } } + }, + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1104,24 +594,9 @@ "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -1143,6 +618,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, @@ -1167,95 +657,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -1263,47 +679,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -1313,36 +710,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" - } - } - }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } } } }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -1350,9 +720,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -1366,10 +733,24 @@ } } }, - "concurrent_connections": { - "type": "long" + "timestamp": { + "type": "date" + }, + "usage": { + "properties": { + "index": { + "type": "keyword" + } + } } } + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, diff --git a/x-pack/test/functional/es_archives/monitoring/singlecluster_lots_of_nodes/mappings.json b/x-pack/test/functional/es_archives/monitoring/singlecluster_lots_of_nodes/mappings.json index 7b75f0d15292d..d709a40befe3e 100644 --- a/x-pack/test/functional/es_archives/monitoring/singlecluster_lots_of_nodes/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/singlecluster_lots_of_nodes/mappings.json @@ -8,96 +8,14 @@ "date_detection": false, "dynamic": "false", "properties": { - "ccr_auto_follow_stats": { - "properties": { - "auto_followed_clusters": { - "properties": { - "cluster_name": { - "type": "keyword" - }, - "last_seen_metadata_version": { - "type": "long" - }, - "time_since_last_check_millis": { - "type": "long" - } - }, - "type": "nested" - }, - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "properties": { - "auto_follow_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - } - }, - "type": "nested" - } - } - }, "ccr_stats": { "properties": { - "bytes_read": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "fatal_exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "follower_aliases_version": { - "type": "long" - }, "follower_global_checkpoint": { "type": "long" }, "follower_index": { "type": "keyword" }, - "follower_mapping_version": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, "leader_global_checkpoint": { "type": "long" }, @@ -107,112 +25,30 @@ "leader_max_seq_no": { "type": "long" }, - "operations_read": { - "type": "long" - }, "operations_written": { "type": "long" }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "read_exceptions": { - "properties": { - "exception": { - "properties": { - "reason": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - } - }, - "type": "nested" - }, "remote_cluster": { "type": "keyword" }, "shard_id": { "type": "integer" }, - "successful_read_requests": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, "time_since_last_read_millis": { "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" } } }, "cluster_state": { "properties": { - "master_node": { - "type": "keyword" - }, - "nodes": { - "type": "object" - }, "nodes_hash": { "type": "integer" - }, - "shards": { - "type": "object" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "cluster_stats": { - "properties": { - "indices": { - "type": "object" - }, - "nodes": { - "type": "object" } } }, "cluster_uuid": { "type": "keyword" }, - "index_recovery": { - "type": "object" - }, "index_stats": { "properties": { "index": { @@ -227,16 +63,6 @@ } } }, - "fielddata": { - "properties": { - "evictions": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -257,22 +83,6 @@ } } }, - "query_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, "refresh": { "properties": { "total_time_in_millis": { @@ -280,66 +90,10 @@ } } }, - "request_cache": { - "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "memory_size_in_bytes": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } - }, "segments": { "properties": { "count": { "type": "integer" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" } } }, @@ -354,18 +108,8 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -393,17 +137,8 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -416,17 +151,8 @@ }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -494,13 +220,6 @@ "properties": { "primaries": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { "index_time_in_millis": { @@ -510,33 +229,13 @@ "type": "long" } } - }, - "search": { - "properties": { - "query_time_in_millis": { - "type": "long" - }, - "query_total": { - "type": "long" - } - } } } }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_time_in_millis": { - "type": "long" - }, "index_total": { "type": "long" } @@ -558,58 +257,10 @@ } } }, - "interval_ms": { - "type": "long" - }, "job_stats": { "properties": { - "data_counts": { - "properties": { - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "input_bytes": { - "type": "long" - }, - "latest_record_timestamp": { - "type": "date" - }, - "processed_record_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - } - } - }, "job_id": { "type": "keyword" - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long" - }, - "model_bytes": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "state": { - "type": "keyword" } } }, @@ -617,13 +268,6 @@ "properties": { "fs": { "properties": { - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, "io_stats": { "properties": { "total": { @@ -631,15 +275,9 @@ "operations": { "type": "long" }, - "read_kilobytes": { - "type": "long" - }, "read_operations": { "type": "long" }, - "write_kilobytes": { - "type": "long" - }, "write_operations": { "type": "long" } @@ -651,12 +289,6 @@ "properties": { "available_in_bytes": { "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "total_in_bytes": { - "type": "long" } } } @@ -664,18 +296,8 @@ }, "indices": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { - "evictions": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" } @@ -696,33 +318,15 @@ }, "query_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, "request_cache": { "properties": { - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, "memory_size_in_bytes": { "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -772,13 +376,6 @@ "type": "long" } } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } } } }, @@ -827,15 +424,9 @@ } } }, - "mlockall": { - "type": "boolean" - }, "node_id": { "type": "keyword" }, - "node_master": { - "type": "boolean" - }, "os": { "properties": { "cgroup": { @@ -845,9 +436,6 @@ "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -865,26 +453,10 @@ }, "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, "usage_nanos": { "type": "long" } } - }, - "memory": { - "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" - } - } } } }, @@ -892,14 +464,8 @@ "properties": { "load_average": { "properties": { - "15m": { - "type": "half_float" - }, "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" } } } @@ -915,12 +481,6 @@ "type": "half_float" } } - }, - "max_file_descriptors": { - "type": "long" - }, - "open_file_descriptors": { - "type": "long" } } }, @@ -933,22 +493,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "generic": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -959,9 +503,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -972,22 +513,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "management": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -998,22 +523,6 @@ }, "rejected": { "type": "long" - }, - "threads": { - "type": "integer" - } - } - }, - "watcher": { - "properties": { - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - }, - "threads": { - "type": "integer" } } }, @@ -1042,12 +551,6 @@ "primary": { "type": "boolean" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "state": { "type": "keyword" } @@ -1055,22 +558,9 @@ }, "source_node": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { "type": "keyword" }, - "timestamp": { - "format": "date_time", - "type": "date" - }, - "transport_address": { - "type": "keyword" - }, "uuid": { "type": "keyword" } diff --git a/x-pack/test/functional/es_archives/monitoring/standalone_cluster/mappings.json b/x-pack/test/functional/es_archives/monitoring/standalone_cluster/mappings.json index e1b952ef647d9..9ad3eef67cbd3 100644 --- a/x-pack/test/functional/es_archives/monitoring/standalone_cluster/mappings.json +++ b/x-pack/test/functional/es_archives/monitoring/standalone_cluster/mappings.json @@ -6,115 +6,47 @@ "date_detection": false, "dynamic": false, "properties": { - "cluster_uuid": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { + "ccr_stats": { "properties": { - "uuid": { - "type": "keyword" + "follower_global_checkpoint": { + "type": "long" }, - "host": { + "follower_index": { "type": "keyword" }, - "transport_address": { - "type": "keyword" + "leader_global_checkpoint": { + "type": "long" }, - "ip": { + "leader_index": { "type": "keyword" }, - "name": { + "leader_max_seq_no": { + "type": "long" + }, + "operations_written": { + "type": "long" + }, + "remote_cluster": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" + "shard_id": { + "type": "integer" + }, + "time_since_last_read_millis": { + "type": "long" } } }, - "indices_stats": { + "cluster_state": { "properties": { - "_all": { - "properties": { - "primaries": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "indexing": { - "properties": { - "index_total": { - "type": "long" - }, - "index_time_in_millis": { - "type": "long" - } - } - }, - "search": { - "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { - "type": "long" - } - } - } - } - } - } + "nodes_hash": { + "type": "integer" } } }, + "cluster_uuid": { + "type": "keyword" + }, "index_stats": { "properties": { "index": { @@ -129,29 +61,12 @@ } } }, - "fielddata": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -166,44 +81,9 @@ } } }, - "query_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "request_cache": { - "properties": { - "memory_size_in_bytes": { - "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" - } - } - }, - "search": { + "refresh": { "properties": { - "query_total": { - "type": "long" - }, - "query_time_in_millis": { + "total_time_in_millis": { "type": "long" } } @@ -212,42 +92,12 @@ "properties": { "count": { "type": "integer" - }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { - "type": "long" - }, - "points_memory_in_bytes": { - "type": "long" - }, - "stored_fields_memory_in_bytes": { - "type": "long" - }, - "term_vectors_memory_in_bytes": { - "type": "long" - }, - "norms_memory_in_bytes": { - "type": "long" - }, - "doc_values_memory_in_bytes": { - "type": "long" - }, - "index_writer_memory_in_bytes": { - "type": "long" - }, - "version_map_memory_in_bytes": { - "type": "long" - }, - "fixed_bit_set_memory_in_bytes": { - "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -256,36 +106,19 @@ }, "total": { "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" } } }, "indexing": { "properties": { - "index_total": { + "index_time_in_millis": { "type": "long" }, - "index_time_in_millis": { + "index_total": { "type": "long" }, "throttle_time_in_millis": { @@ -304,14 +137,12 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { + } + } + }, + "refresh": { + "properties": { + "total_time_in_millis": { "type": "long" } } @@ -320,24 +151,15 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, "search": { "properties": { - "query_total": { + "query_time_in_millis": { "type": "long" }, - "query_time_in_millis": { + "query_total": { "type": "long" } } @@ -347,41 +169,41 @@ "count": { "type": "integer" }, - "memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "terms_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "norms_memory_in_bytes": { "type": "long" }, - "norms_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "version_map_memory_in_bytes": { "type": "long" } } }, - "refresh": { + "store": { "properties": { - "total_time_in_millis": { + "size_in_bytes": { "type": "long" } } @@ -390,68 +212,92 @@ } } }, - "cluster_stats": { + "indices_stats": { "properties": { - "nodes": { - "type": "object" - }, - "indices": { - "type": "object" + "_all": { + "properties": { + "primaries": { + "properties": { + "indexing": { + "properties": { + "index_time_in_millis": { + "type": "long" + }, + "index_total": { + "type": "long" + } + } + } + } + }, + "total": { + "properties": { + "indexing": { + "properties": { + "index_total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "query_time_in_millis": { + "type": "long" + }, + "query_total": { + "type": "long" + } + } + } + } + } + } } } }, - "cluster_state": { + "job_stats": { "properties": { - "version": { - "type": "long" - }, - "nodes_hash": { - "type": "integer" - }, - "master_node": { - "type": "keyword" - }, - "state_uuid": { - "type": "keyword" - }, - "status": { + "job_id": { "type": "keyword" - }, - "nodes": { - "type": "object" - }, - "shards": { - "type": "object" } } }, "node_stats": { "properties": { - "node_id": { - "type": "keyword" - }, - "node_master": { - "type": "boolean" - }, - "mlockall": { - "type": "boolean" - }, - "indices": { + "fs": { "properties": { - "docs": { + "io_stats": { "properties": { - "count": { - "type": "long" + "total": { + "properties": { + "operations": { + "type": "long" + }, + "read_operations": { + "type": "long" + }, + "write_operations": { + "type": "long" + } + } } } }, + "total": { + "properties": { + "available_in_bytes": { + "type": "long" + } + } + } + } + }, + "indices": { + "properties": { "fielddata": { "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" } } }, @@ -472,15 +318,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -488,15 +325,6 @@ "properties": { "memory_size_in_bytes": { "type": "long" - }, - "evictions": { - "type": "long" - }, - "hit_count": { - "type": "long" - }, - "miss_count": { - "type": "long" } } }, @@ -515,116 +343,97 @@ "count": { "type": "integer" }, - "memory_in_bytes": { - "type": "long" - }, - "terms_memory_in_bytes": { + "doc_values_memory_in_bytes": { "type": "long" }, - "points_memory_in_bytes": { + "fixed_bit_set_memory_in_bytes": { "type": "long" }, - "stored_fields_memory_in_bytes": { + "index_writer_memory_in_bytes": { "type": "long" }, - "term_vectors_memory_in_bytes": { + "memory_in_bytes": { "type": "long" }, "norms_memory_in_bytes": { "type": "long" }, - "doc_values_memory_in_bytes": { + "points_memory_in_bytes": { "type": "long" }, - "index_writer_memory_in_bytes": { + "stored_fields_memory_in_bytes": { "type": "long" }, - "version_map_memory_in_bytes": { + "term_vectors_memory_in_bytes": { "type": "long" }, - "fixed_bit_set_memory_in_bytes": { + "terms_memory_in_bytes": { "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { + }, + "version_map_memory_in_bytes": { "type": "long" } } } } }, - "fs": { + "jvm": { "properties": { - "total": { - "properties": { - "total_in_bytes": { - "type": "long" - }, - "free_in_bytes": { - "type": "long" - }, - "available_in_bytes": { - "type": "long" - } - } - }, - "data": { - "properties": { - "spins": { - "type": "boolean" - } - } - }, - "io_stats": { + "gc": { "properties": { - "total": { + "collectors": { "properties": { - "operations": { - "type": "long" - }, - "read_operations": { - "type": "long" - }, - "write_operations": { - "type": "long" - }, - "read_kilobytes": { - "type": "long" + "old": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } }, - "write_kilobytes": { - "type": "long" + "young": { + "properties": { + "collection_count": { + "type": "long" + }, + "collection_time_in_millis": { + "type": "long" + } + } } } } } + }, + "mem": { + "properties": { + "heap_max_in_bytes": { + "type": "long" + }, + "heap_used_in_bytes": { + "type": "long" + }, + "heap_used_percent": { + "type": "half_float" + } + } } } }, + "node_id": { + "type": "keyword" + }, "os": { "properties": { "cgroup": { "properties": { - "cpuacct": { - "properties": { - "control_group": { - "type": "keyword" - }, - "usage_nanos": { - "type": "long" - } - } - }, "cpu": { "properties": { "cfs_quota_micros": { "type": "long" }, - "control_group": { - "type": "keyword" - }, "stat": { "properties": { "number_of_elapsed_periods": { @@ -640,16 +449,10 @@ } } }, - "memory": { + "cpuacct": { "properties": { - "control_group": { - "type": "keyword" - }, - "limit_in_bytes": { - "type": "keyword" - }, - "usage_in_bytes": { - "type": "keyword" + "usage_nanos": { + "type": "long" } } } @@ -661,12 +464,6 @@ "properties": { "1m": { "type": "half_float" - }, - "5m": { - "type": "half_float" - }, - "15m": { - "type": "half_float" } } } @@ -676,12 +473,6 @@ }, "process": { "properties": { - "open_file_descriptors": { - "type": "long" - }, - "max_file_descriptors": { - "type": "long" - }, "cpu": { "properties": { "percent": { @@ -691,110 +482,30 @@ } } }, - "jvm": { + "thread_pool": { "properties": { - "mem": { + "bulk": { "properties": { - "heap_used_in_bytes": { - "type": "long" + "queue": { + "type": "integer" }, - "heap_used_percent": { - "type": "half_float" + "rejected": { + "type": "long" + } + } + }, + "get": { + "properties": { + "queue": { + "type": "integer" }, - "heap_max_in_bytes": { + "rejected": { "type": "long" } } }, - "gc": { + "index": { "properties": { - "collectors": { - "properties": { - "young": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - }, - "old": { - "properties": { - "collection_count": { - "type": "long" - }, - "collection_time_in_millis": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "thread_pool": { - "properties": { - "bulk": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "generic": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "get": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "index": { - "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "management": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -805,22 +516,6 @@ }, "search": { "properties": { - "threads": { - "type": "integer" - }, - "queue": { - "type": "integer" - }, - "rejected": { - "type": "long" - } - } - }, - "watcher": { - "properties": { - "threads": { - "type": "integer" - }, "queue": { "type": "integer" }, @@ -843,246 +538,41 @@ } } }, - "index_recovery": { - "type": "object" - }, "shard": { "properties": { - "state": { - "type": "keyword" - }, - "primary": { - "type": "boolean" - }, "index": { "type": "keyword" }, - "relocating_node": { - "type": "keyword" - }, - "shard": { - "type": "long" - }, "node": { "type": "keyword" - } - } - }, - "job_stats": { - "properties": { - "job_id": { - "type": "keyword" + }, + "primary": { + "type": "boolean" }, "state": { "type": "keyword" - }, - "data_counts": { - "properties": { - "input_bytes": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "empty_bucket_count": { - "type": "long" - }, - "sparse_bucket_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - } - } - }, - "model_size_stats": { - "properties": { - "model_bytes": { - "type": "long" - }, - "bucket_allocation_failures_count": { - "type": "long" - } - } - }, - "node": { - "properties": { - "id": { - "type": "keyword" - } - } } } }, - "ccr_stats": { + "source_node": { "properties": { - "remote_cluster": { - "type": "keyword" - }, - "leader_index": { + "name": { "type": "keyword" }, - "follower_index": { + "uuid": { "type": "keyword" - }, - "shard_id": { - "type": "integer" - }, - "leader_global_checkpoint": { - "type": "long" - }, - "leader_max_seq_no": { - "type": "long" - }, - "follower_global_checkpoint": { - "type": "long" - }, - "follower_max_seq_no": { - "type": "long" - }, - "last_requested_seq_no": { - "type": "long" - }, - "outstanding_read_requests": { - "type": "long" - }, - "outstanding_write_requests": { - "type": "long" - }, - "write_buffer_operation_count": { - "type": "long" - }, - "write_buffer_size_in_bytes": { - "type": "long" - }, - "follower_mapping_version": { - "type": "long" - }, - "follower_settings_version": { - "type": "long" - }, - "total_read_time_millis": { - "type": "long" - }, - "total_read_remote_exec_time_millis": { - "type": "long" - }, - "successful_read_requests": { - "type": "long" - }, - "failed_read_requests": { - "type": "long" - }, - "operations_read": { - "type": "long" - }, - "bytes_read": { - "type": "long" - }, - "total_write_time_millis": { - "type": "long" - }, - "successful_write_requests": { - "type": "long" - }, - "failed_write_requests": { - "type": "long" - }, - "operations_written": { - "type": "long" - }, - "read_exceptions": { - "type": "nested", - "properties": { - "from_seq_no": { - "type": "long" - }, - "retries": { - "type": "integer" - }, - "exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "time_since_last_read_millis": { - "type": "long" - }, - "fatal_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } } } }, - "ccr_auto_follow_stats": { - "properties": { - "number_of_failed_follow_indices": { - "type": "long" - }, - "number_of_failed_remote_cluster_state_requests": { - "type": "long" - }, - "number_of_successful_follow_indices": { - "type": "long" - }, - "recent_auto_follow_errors": { - "type": "nested", - "properties": { - "leader_index": { - "type": "keyword" - }, - "timestamp": { - "type": "long" - }, - "auto_follow_exception": { - "type": "object", - "properties": { - "type": { - "type": "keyword" - }, - "reason": { - "type": "text" - } - } - } - } - }, - "auto_followed_clusters": { - "type": "nested", - "properties": { - "cluster_name": { - "type": "keyword" - }, - "time_since_last_check_millis": { - "type": "long" - }, - "last_seen_metadata_version": { - "type": "long" - } - } - } - } + "state_uuid": { + "type": "keyword" + }, + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" } } }, @@ -1104,24 +594,9 @@ "mappings": { "dynamic": false, "properties": { - "timestamp": { - "type": "date" - }, - "update_timestamp": { - "type": "date" - }, - "resolved_timestamp": { - "type": "date" - }, - "prefix": { - "type": "text" - }, "message": { "type": "text" }, - "suffix": { - "type": "text" - }, "metadata": { "properties": { "cluster_uuid": { @@ -1143,6 +618,21 @@ "type": "keyword" } } + }, + "prefix": { + "type": "text" + }, + "resolved_timestamp": { + "type": "date" + }, + "suffix": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "update_timestamp": { + "type": "date" } } }, @@ -1167,95 +657,21 @@ "cluster_uuid": { "type": "keyword" }, - "timestamp": { - "type": "date", - "format": "date_time" - }, - "interval_ms": { - "type": "long" - }, - "type": { - "type": "keyword" - }, - "source_node": { - "properties": { - "uuid": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "date_time" - } - } - }, "kibana_stats": { "properties": { + "concurrent_connections": { + "type": "long" + }, "kibana": { "properties": { - "uuid": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "snapshot": { - "type": "boolean" - }, "status": { "type": "keyword" }, - "statuses": { - "properties": { - "name": { - "type": "keyword" - }, - "state": { - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "vm_type": { - "type": "keyword" - }, - "region": { + "uuid": { "type": "keyword" }, - "zone": { + "version": { "type": "keyword" - }, - "metadata": { - "type": "object" } } }, @@ -1263,47 +679,28 @@ "properties": { "load": { "properties": { - "1m": { + "15m": { "type": "half_float" }, - "5m": { + "1m": { "type": "half_float" }, - "15m": { + "5m": { "type": "half_float" } } - }, - "memory": { - "properties": { - "total_in_bytes": { - "type": "float" - }, - "free_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - } - } - }, - "uptime_in_millis": { - "type": "long" } } }, "process": { "properties": { + "event_loop_delay": { + "type": "float" + }, "memory": { "properties": { "heap": { "properties": { - "total_in_bytes": { - "type": "float" - }, - "used_in_bytes": { - "type": "float" - }, "size_limit": { "type": "float" } @@ -1313,36 +710,9 @@ "type": "float" } } - }, - "event_loop_delay": { - "type": "float" - }, - "uptime_in_millis": { - "type": "long" } } }, - "sockets": { - "properties": { - "http": { - "properties": { - "total": { - "type": "long" - } - } - }, - "https": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "timestamp": { - "type": "date" - }, "requests": { "properties": { "disconnects": { @@ -1350,9 +720,6 @@ }, "total": { "type": "long" - }, - "status_codes": { - "type": "object" } } }, @@ -1366,531 +733,164 @@ } } }, - "concurrent_connections": { - "type": "long" - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "format": "6", - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} - -{ - "type": "index", - "value": { - "index": ".monitoring-beats-6-2019.02.04", - "mappings": { - "dynamic": false, - "properties": { - "beats_state": { - "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } + "timestamp": { + "type": "date" }, - "state": { + "usage": { "properties": { - "beat": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "type": "keyword" - }, - "family": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - } - } - }, - "input": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "module": { - "properties": { - "count": { - "type": "long" - }, - "names": { - "type": "keyword" - } - } - }, - "output": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "service": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } + "index": { + "type": "keyword" } } - }, - "timestamp": { - "format": "date_time", - "type": "date" } } }, - "beats_stats": { - "properties": { - "beat": { - "properties": { - "host": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "metrics": { - "properties": { - "beat": { - "properties": { - "cpu": { - "properties": { - "system": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "total": { - "properties": { - "value": { - "type": "long" - }, - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "user": { - "properties": { - "ticks": { - "type": "long" - }, - "time": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - } - } - }, - "info": { - "properties": { - "ephemeral_id": { - "type": "keyword" - }, - "uptime": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "memstats": { - "properties": { - "gc_next": { - "type": "long" - }, - "memory_alloc": { - "type": "long" - }, - "memory_total": { - "type": "long" - }, - "rss": { - "type": "long" - } - } - }, - "handles": { - "properties": { - "open": { - "type": "long" - }, - "limit": { - "properties": { - "hard": { - "type": "long" - }, - "soft": { - "type": "long" - } - } - } - } - } - } - }, - "apm-server": { - "properties": { - "server": { - "properties": { - "request": { - "properties": { - "count": { - "type": "long" - } - } - }, - "concurrent": { - "properties": { - "wait": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "response": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "properties": { - "count": { - "type": "long" - }, - "toolarge": { - "type": "long" - }, - "validate": { - "type": "long" - }, - "ratelimit": { - "type": "long" - }, - "queue": { - "type": "long" - }, - "closed": { - "type": "long" - }, - "forbidden": { - "type": "long" - }, - "concurrency": { - "type": "long" - }, - "unauthorized": { - "type": "long" - }, - "internal": { - "type": "long" - }, - "decode": { - "type": "long" - }, - "method": { - "type": "long" - } - } - }, - "valid": { - "properties": { - "ok": { - "type": "long" - }, - "accepted": { - "type": "long" - }, - "count": { - "type": "long" - } - } - } - } - } - } - }, - "decoder": { + "timestamp": { + "format": "date_time", + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "format": "6", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "index": ".monitoring-beats-6-2019.02.04", + "mappings": { + "dynamic": false, + "properties": { + "beats_state": { + "properties": { + "timestamp": { + "format": "date_time", + "type": "date" + } + } + }, + "beats_stats": { + "properties": { + "beat": { + "properties": { + "type": { + "type": "keyword" + }, + "uuid": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "metrics": { + "properties": { + "apm-server": { + "properties": { + "processor": { "properties": { - "deflate": { - "properties": { - "content-length": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "gzip": { + "error": { "properties": { - "content-length": { - "type": "long" - }, - "count": { + "transformations": { "type": "long" } } }, - "uncompressed": { + "metric": { "properties": { - "content-length": { - "type": "long" - }, - "count": { + "transformations": { "type": "long" } } }, - "reader": { + "span": { "properties": { - "size": { - "type": "long" - }, - "count": { + "transformations": { "type": "long" } } }, - "missing-content-length": { + "transaction": { "properties": { - "count": { + "transformations": { "type": "long" } } } } }, - "processor": { + "server": { "properties": { - "metric": { + "request": { "properties": { - "decoding": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { - "type": "long" - }, - "count": { - "type": "long" - } - } - }, - "transformations": { + "count": { "type": "long" } } }, - "sourcemap": { + "response": { "properties": { - "counter": { + "count": { "type": "long" }, - "decoding": { + "errors": { "properties": { - "errors": { + "closed": { "type": "long" }, - "count": { + "concurrency": { "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { + }, + "decode": { "type": "long" }, - "count": { + "forbidden": { "type": "long" - } - } - } - } - }, - "transaction": { - "properties": { - "decoding": { - "properties": { - "errors": { + }, + "internal": { "type": "long" }, - "count": { + "method": { "type": "long" - } - } - }, - "validation": { - "properties": { - "errors": { + }, + "queue": { "type": "long" }, - "count": { + "ratelimit": { "type": "long" - } - } - }, - "transformations": { - "type": "long" - }, - "transactions": { - "type": "long" - }, - "spans": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "error": { - "properties": { - "decoding": { - "properties": { - "errors": { + }, + "toolarge": { + "type": "long" + }, + "unauthorized": { "type": "long" }, - "count": { + "validate": { "type": "long" } } }, - "validation": { + "valid": { "properties": { - "errors": { + "accepted": { "type": "long" }, - "count": { + "ok": { "type": "long" } } - }, - "transformations": { - "type": "long" - }, - "errors": { - "type": "long" - }, - "stacktraces": { - "type": "long" - }, - "frames": { - "type": "long" - } - } - }, - "span": { - "properties": { - "transformations": { - "type": "long" } } } @@ -1898,28 +898,53 @@ } } }, - "libbeat": { + "beat": { "properties": { - "config": { + "cpu": { "properties": { - "module": { + "total": { "properties": { - "running": { - "type": "long" - }, - "starts": { - "type": "long" - }, - "stops": { + "value": { "type": "long" } } - }, - "reloads": { + } + } + }, + "handles": { + "properties": { + "open": { "type": "long" } } }, + "info": { + "properties": { + "ephemeral_id": { + "type": "keyword" + } + } + }, + "memstats": { + "properties": { + "gc_next": { + "type": "long" + }, + "memory_alloc": { + "type": "long" + }, + "memory_total": { + "type": "long" + }, + "rss": { + "type": "long" + } + } + } + } + }, + "libbeat": { + "properties": { "output": { "properties": { "events": { @@ -1930,23 +955,14 @@ "active": { "type": "long" }, - "batches": { - "type": "long" - }, "dropped": { "type": "long" }, - "duplicates": { - "type": "long" - }, "failed": { "type": "long" }, "total": { "type": "long" - }, - "toomany": { - "type": "long" } } }, @@ -1960,9 +976,6 @@ } } }, - "type": { - "type": "keyword" - }, "write": { "properties": { "bytes": { @@ -1977,23 +990,14 @@ }, "pipeline": { "properties": { - "clients": { - "type": "long" - }, "events": { "properties": { - "active": { - "type": "long" - }, "dropped": { "type": "long" }, "failed": { "type": "long" }, - "filtered": { - "type": "long" - }, "published": { "type": "long" }, @@ -2004,13 +1008,6 @@ "type": "long" } } - }, - "queue": { - "properties": { - "acked": { - "type": "long" - } - } } } } @@ -2023,24 +1020,11 @@ "1": { "type": "double" }, - "5": { - "type": "double" - }, "15": { "type": "double" }, - "norm": { - "properties": { - "1": { - "type": "double" - }, - "5": { - "type": "double" - }, - "15": { - "type": "double" - } - } + "5": { + "type": "double" } } } @@ -2048,9 +1032,6 @@ } } }, - "tags": { - "type": "keyword" - }, "timestamp": { "format": "date_time", "type": "date" @@ -2060,25 +1041,16 @@ "cluster_uuid": { "type": "keyword" }, - "interval_ms": { - "type": "long" - }, - "source_node": { + "metricset": { "properties": { - "host": { - "type": "keyword" - }, - "ip": { - "type": "keyword" - }, "name": { - "type": "keyword" - }, - "transport_address": { - "type": "keyword" - }, - "uuid": { - "type": "keyword" + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz new file mode 100644 index 0000000000000..06e83f8c267d6 Binary files /dev/null and b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/mappings.json b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/mappings.json new file mode 100644 index 0000000000000..1de04a64398c4 --- /dev/null +++ b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/mappings.json @@ -0,0 +1,2635 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "74eb4b909f81222fa1ddeaba2881a37e", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "epm-packages": "386dc9996a3b74607de64c2ab2171582", + "exception-list": "497afa2f881a675d72d58e20057f3d8b", + "exception-list-agnostic": "497afa2f881a675d72d58e20057f3d8b", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "6012d61d15e72564e47fc3402332756e", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "ingest-package-policies": "f74dfe498e1849267cda41580b2be110", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "7f9e077078cab612f6a58e3bfdedb71a", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "44d6bd48a1a653bcb60ea01614b9e3c9", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "agent_actions": { + "dynamic": "false", + "type": "object" + }, + "agent_configs": { + "dynamic": "false", + "type": "object" + }, + "agent_events": { + "dynamic": "false", + "type": "object" + }, + "agents": { + "dynamic": "false", + "type": "object" + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-services-telemetry": { + "dynamic": "false", + "type": "object" + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "datasources": { + "dynamic": "false", + "type": "object" + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "enrollment_api_keys": { + "dynamic": "false", + "type": "object" + }, + "epm-package": { + "dynamic": "false", + "type": "object" + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "install_started_at": { + "type": "date" + }, + "install_status": { + "type": "keyword" + }, + "install_version": { + "type": "keyword" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "policy_id": { + "type": "keyword" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "inventoryDefaultView": { + "type": "keyword" + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_policies": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "policy_id": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "source": { + "type": "keyword" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "outputs": { + "dynamic": "false", + "type": "object" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "server": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "dynamic": "false", + "type": "object" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file 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 ce356dbd081c8..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 @@ -115,7 +115,7 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(400); - expect(apiResponse.message).to.match(/Agent version is not compatible with kibana/); + expect(apiResponse.message).to.match(/is not compatible/); }); it('should allow to enroll an agent with a valid enrollment token', async () => { @@ -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/licensing_plugin/config.legacy.ts b/x-pack/test/licensing_plugin/config.legacy.ts deleted file mode 100644 index 14eadc3194f9e..0000000000000 --- a/x-pack/test/licensing_plugin/config.legacy.ts +++ /dev/null @@ -1,15 +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 { FtrConfigProviderContext } from '@kbn/test/types/ftr'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const commonConfig = await readConfigFile(require.resolve('./config')); - - return { - ...commonConfig.getAll(), - testFiles: [require.resolve('./legacy')], - }; -} diff --git a/x-pack/test/licensing_plugin/legacy/index.ts b/x-pack/test/licensing_plugin/legacy/index.ts deleted file mode 100644 index 6274bd3969042..0000000000000 --- a/x-pack/test/licensing_plugin/legacy/index.ts +++ /dev/null @@ -1,16 +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 { FtrProviderContext } from '../services'; - -// eslint-disable-next-line import/no-default-export -export default function ({ loadTestFile }: FtrProviderContext) { - describe('Legacy licensing plugin', function () { - this.tags('ciGroup2'); - // MUST BE LAST! CHANGES LICENSE TYPE! - loadTestFile(require.resolve('./updates')); - }); -} diff --git a/x-pack/test/licensing_plugin/legacy/updates.ts b/x-pack/test/licensing_plugin/legacy/updates.ts deleted file mode 100644 index 1de8659672d2f..0000000000000 --- a/x-pack/test/licensing_plugin/legacy/updates.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../services'; -import { createScenario } from '../scenario'; -import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; - -// eslint-disable-next-line import/no-default-export -export default function (ftrContext: FtrProviderContext) { - const { getService } = ftrContext; - const supertest = getService('supertest'); - const testSubjects = getService('testSubjects'); - - const scenario = createScenario(ftrContext); - - describe('changes in license types', () => { - after(async () => { - await scenario.teardown(); - }); - - it('provides changes in license types', async () => { - await scenario.setup(); - await scenario.waitForPluginToDetectLicenseUpdate(); - - const { - body: legacyInitialLicense, - header: legacyInitialLicenseHeaders, - } = await supertest.get('/api/xpack/v1/info').expect(200); - - expect(legacyInitialLicense.license?.type).to.be('basic'); - expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); - - await scenario.startTrial(); - await scenario.waitForPluginToDetectLicenseUpdate(); - - const { body: legacyTrialLicense, header: legacyTrialLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); - - expect(legacyTrialLicense.license?.type).to.be('trial'); - expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - await scenario.startBasic(); - await scenario.waitForPluginToDetectLicenseUpdate(); - - const { body: legacyBasicLicense } = await supertest.get('/api/xpack/v1/info').expect(200); - expect(legacyBasicLicense.license?.type).to.be('basic'); - - // banner shown only when license expired not just deleted - await testSubjects.missingOrFail('licenseExpiredBanner'); - }); - }); -} diff --git a/x-pack/test/licensing_plugin/public/feature_usage.ts b/x-pack/test/licensing_plugin/public/feature_usage.ts new file mode 100644 index 0000000000000..15d302d71bfab --- /dev/null +++ b/x-pack/test/licensing_plugin/public/feature_usage.ts @@ -0,0 +1,82 @@ +/* + * 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 '../services'; +import { + LicensingPluginSetup, + LicensingPluginStart, + LicenseType, +} from '../../../plugins/licensing/public'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +interface FeatureUsage { + last_used?: number; + license_level: LicenseType; + name: string; +} + +// eslint-disable-next-line import/no-default-export +export default function (ftrContext: FtrProviderContext) { + const { getService, getPageObjects } = ftrContext; + const supertest = getService('supertest'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'security']); + + const registerFeature = async (featureName: string, licenseType: LicenseType) => { + await browser.executeAsync( + async (feature, type, cb) => { + const { setup } = window._coreProvider; + const licensing: LicensingPluginSetup = setup.plugins.licensing; + await licensing.featureUsage.register(feature, type); + cb(); + }, + featureName, + licenseType + ); + }; + + const notifyFeatureUsage = async (featureName: string, lastUsed: number) => { + await browser.executeAsync( + async (feature, time, cb) => { + const { start } = window._coreProvider; + const licensing: LicensingPluginStart = start.plugins.licensing; + await licensing.featureUsage.notifyUsage(feature, time); + cb(); + }, + featureName, + lastUsed + ); + }; + + describe('feature_usage API', () => { + before(async () => { + await PageObjects.security.login(); + }); + + it('allows to register features to the server', async () => { + await registerFeature('test-client-A', 'gold'); + await registerFeature('test-client-B', 'enterprise'); + + const response = await supertest.get('/api/licensing/feature_usage').expect(200); + const features = response.body.features.map(({ name }: FeatureUsage) => name); + + expect(features).to.contain('test-client-A'); + expect(features).to.contain('test-client-B'); + }); + + it('allows to notify feature usage', async () => { + const now = new Date(); + + await notifyFeatureUsage('test-client-A', now.getTime()); + + const response = await supertest.get('/api/licensing/feature_usage').expect(200); + const features = response.body.features as FeatureUsage[]; + + expect(features.find((f) => f.name === 'test-client-A')?.last_used).to.be(now.toISOString()); + expect(features.find((f) => f.name === 'test-client-B')?.last_used).to.be(null); + }); + }); +} diff --git a/x-pack/test/licensing_plugin/public/index.ts b/x-pack/test/licensing_plugin/public/index.ts index 86a3c21cfdb39..268a74c56bd72 100644 --- a/x-pack/test/licensing_plugin/public/index.ts +++ b/x-pack/test/licensing_plugin/public/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../services'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Licensing plugin public client', function () { this.tags('ciGroup2'); + loadTestFile(require.resolve('./feature_usage')); // MUST BE LAST! CHANGES LICENSE TYPE! loadTestFile(require.resolve('./updates')); }); 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.config.ts b/x-pack/test/reporting_api_integration/reporting_and_security.config.ts index dc5aaba69604f..ee165bf45fc7e 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security.config.ts @@ -44,7 +44,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.reporting.csv.maxSizeBytes=2850`, `--xpack.reporting.queue.pollInterval=3000`, `--xpack.security.session.idleTimeout=3600000`, - `--xpack.spaces.enabled=false`, `--xpack.reporting.capture.networkPolicy.rules=${JSON.stringify(testPolicyRules)}`, ], }, diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts index 18ef28150c42d..8bf4d2448498c 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts @@ -12,7 +12,8 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags('ciGroup2'); loadTestFile(require.resolve('./csv_job_params')); loadTestFile(require.resolve('./csv_saved_search')); - loadTestFile(require.resolve('./usage')); loadTestFile(require.resolve('./network_policy')); + loadTestFile(require.resolve('./spaces')); + loadTestFile(require.resolve('./usage')); }); } diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts b/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts index 9f9800cafb99a..8692f79d5aea9 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts @@ -20,7 +20,7 @@ export default function ({ getService }: FtrProviderContext) { * The Reporting API Functional Test config implements a network policy that * is designed to disallow the following Canvas worksheet */ - describe('reporting network policy', () => { + describe('Network Policy', () => { before(async () => { await esArchiver.load(archive); // includes a canvas worksheet with an offending image URL }); 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 new file mode 100644 index 0000000000000..6a68bd530cf63 --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts @@ -0,0 +1,77 @@ +/* + * 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 * as Rx from 'rxjs'; +import { filter, first, map, switchMap, tap, timeout } from 'rxjs/operators'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const reportingAPI = getService('reportingAPI'); + const supertest = getService('supertest'); + const log = getService('log'); + + const getCompleted$ = (downloadPath: string) => { + return Rx.interval(2000).pipe( + tap(() => log.debug(`checking report status at ${downloadPath}...`)), + switchMap(() => supertest.get(downloadPath)), + filter(({ status: statusCode }) => statusCode === 200), + map((response) => response.text), + first(), + timeout(15000) + ); + }; + + // 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 + }); + + after(async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana_spaces'); + }); + + afterEach(async () => { + await reportingAPI.deleteAllReports(); + }); + + it('should complete a job of CSV saved search export in non-default space', async () => { + const downloadPath = await reportingAPI.postJob( + `/s/non_default_space/api/reporting/generate/csv?jobParams=%28browserTimezone%3AUTC%2CconflictedTypesFields%3A%21%28%29%2Cfields%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%2CindexPatternId%3A%27067dec90-e7ee-11ea-a730-d58e9ea7581b%27%2CmetaFields%3A%21%28_source%2C_id%2C_type%2C_index%2C_score%29%2CobjectType%3Asearch%2CsearchRequest%3A%28body%3A%28_source%3A%28includes%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%29%2Cdocvalue_fields%3A%21%28%28field%3Aorder_date%2Cformat%3Adate_time%29%29%2Cquery%3A%28bool%3A%28filter%3A%21%28%28match_all%3A%28%29%29%2C%28range%3A%28order_date%3A%28format%3Astrict_date_optional_time%2Cgte%3A%272019-06-11T08%3A24%3A16.425Z%27%2Clte%3A%272019-07-13T09%3A31%3A07.520Z%27%29%29%29%29%2Cmust%3A%21%28%29%2Cmust_not%3A%21%28%29%2Cshould%3A%21%28%29%29%29%2Cscript_fields%3A%28%29%2Csort%3A%21%28%28order_date%3A%28order%3Adesc%2Cunmapped_type%3Aboolean%29%29%29%2Cstored_fields%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%2Cversion%3A%21t%29%2Cindex%3A%27ecommerce%2A%27%29%2Ctitle%3A%27Ecom%20Search%27%29` + ); + + // Retry the download URL until a "completed" response status is returned + const completed$ = getCompleted$(downloadPath); + const reportCompleted = await completed$.toPromise(); + expect(reportCompleted).to.match(/^"order_date",/); + }); + + it('should complete a job of PNG export of a dashboard in non-default space', async () => { + const downloadPath = await reportingAPI.postJob( + `/s/non_default_space/api/reporting/generate/png?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apng%29%2CobjectType%3Adashboard%2CrelativeUrl%3A%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29` + ); + + const completed$: Rx.Observable = getCompleted$(downloadPath); + const reportCompleted = await completed$.toPromise(); + expect(reportCompleted).to.not.be(null); + }); + + it('should complete a job of PDF export of a dashboard in non-default space', async () => { + const downloadPath = await reportingAPI.postJob( + `/s/non_default_space/api/reporting/generate/printablePdf?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apreserve_layout%29%2CobjectType%3Adashboard%2CrelativeUrls%3A%21%28%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%29%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29` + ); + + const completed$ = getCompleted$(downloadPath); + const reportCompleted = await completed$.toPromise(); + expect(reportCompleted).to.not.be(null); + }); + }); +} 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 24e68b3917d6c..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('reporting 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 3e04a507d3810..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: { @@ -142,6 +153,42 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, }, // The manifest version could have changed when the Policy was updated because the // policy details page ensures that a save action applies the udpated policy on top @@ -186,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/test/ui_capabilities/common/services/ui_capabilities.ts b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts index 7f831973aea5c..e567828598b31 100644 --- a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts +++ b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import axios, { AxiosInstance } from 'axios'; -import { UICapabilities } from 'ui/capabilities'; +import type { Capabilities as UICapabilities } from 'src/core/types'; import { format as formatUrl } from 'url'; import util from 'util'; import { ToolingLog } from '@kbn/dev-utils'; diff --git a/x-pack/test_utils/jest/config.js b/x-pack/test_utils/jest/config.js index 7bb073023b7f8..c94fe02d2f4bd 100644 --- a/x-pack/test_utils/jest/config.js +++ b/x-pack/test_utils/jest/config.js @@ -17,7 +17,6 @@ export default { ], collectCoverageFrom: ['legacy/plugins/**/*.js', 'legacy/common/**/*.js', 'legacy/server/**/*.js'], moduleNameMapper: { - '^ui/(.*)': '**/public/$1', '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/../src/dev/jest/mocks/style_mock.js', diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index e978702a35634..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/**/*", @@ -14,17 +14,13 @@ "test/**/*", "plugins/security_solution/cypress/**/*", "plugins/apm/e2e/cypress/**/*", - "plugins/apm/scripts/**/*", - "**/typespec_tests.ts" + "plugins/apm/scripts/**/*" ], "compilerOptions": { "outDir": ".", "paths": { "kibana/public": ["src/core/public"], "kibana/server": ["src/core/server"], - "ui/*": [ - "src/legacy/ui/public/*" - ], "plugins/xpack_main/*": [ "x-pack/legacy/plugins/xpack_main/public/*" ], @@ -37,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 496331aba0485..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== @@ -1144,10 +1144,10 @@ dependencies: "@elastic/apm-rum-core" "^5.6.0" -"@elastic/charts@19.8.1": - version "19.8.1" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.8.1.tgz#27653823911c26e4703c73588367473215beaf0f" - integrity sha512-vONCrcZ8bZ+C16+bKLoLyNrMC/b2UvYNoPbYcnB5XYAg5a68finvXEcWD6Y+qa7GLaO2CMe5J9eSjLWXHHDmLg== +"@elastic/charts@21.0.1": + version "21.0.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-21.0.1.tgz#1e8be5303d0c7d53d7ccfcfa121ef68164ae200f" + integrity sha512-KMJE2JNwoy021Rvhgu1wiga0FVCa0u4NlFVUSR+h+G010KLarc+c9yUKBTG8v/nZ6ijBtuOLCjjU9OCWXYfxvA== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -1229,10 +1229,10 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@27.4.1": - version "27.4.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-27.4.1.tgz#9803f6f6c6997c769720381f521e5319ef07a599" - integrity sha512-eGztErFlvkwI4BFxDs6tFBGFiN15NQdJdRHvSi5caOQFGabuR8XCRNUWcNFHlwKVJaDaGsJRuwbbSLh9XdWubQ== +"@elastic/eui@28.2.0": + version "28.2.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-28.2.0.tgz#0c89c7adc27d4ac918191af8237a2634a26d0637" + integrity sha512-ovVkwbwcJLs87ycaof5pyTGjpuPH2sN38k2e+c3IiZjoeGAqWZJouptQguoKYglCyayxoBUV3TAE82m+qyxCnA== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.116" @@ -1241,6 +1241,7 @@ "@types/react-input-autosize" "^2.0.2" "@types/react-virtualized-auto-sizer" "^1.0.0" "@types/react-window" "^1.8.1" + "@types/vfile-message" "^2.0.0" chroma-js "^2.0.4" classnames "^2.2.5" highlight.js "^9.12.0" @@ -1251,15 +1252,25 @@ prop-types "^15.6.0" react-ace "^7.0.5" react-beautiful-dnd "^13.0.0" - react-focus-on "^3.4.1" + react-dropzone "^10.2.1" + react-focus-on "^3.5.0" react-input-autosize "^2.2.2" react-is "~16.3.0" react-virtualized-auto-sizer "^1.0.2" react-window "^1.8.5" + rehype-raw "^4.0.1" + rehype-react "^6.0.0" + rehype-stringify "^6.0.1" + remark-emoji "^2.1.0" + remark-highlight.js "^5.2.0" + remark-parse "^7.0.2" + remark-rehype "^7.0.0" resize-observer-polyfill "^1.5.0" tabbable "^3.0.0" text-diff "^1.0.1" + unified "^8.4.2" uuid "^3.1.0" + vfile "^4.1.1" "@elastic/filesaver@1.1.2": version "1.1.2" @@ -2182,6 +2193,13 @@ vfile "^4.0.0" vfile-reporter "^5.1.1" +"@mapbox/hast-util-table-cell-style@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.1.3.tgz#5b7166ae01297d72216932b245e4b2f0b642dca6" + integrity sha512-QsEsh5YaDvHoMQ2YHdvZy2iDnU3GgKVBTcHf6cILyoWDZtPSdlG444pL/ioPYO/GpXSfODBb9sefEetfC4v9oA== + dependencies: + unist-util-visit "^1.3.0" + "@mapbox/jsonlint-lines-primitives@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" @@ -3643,13 +3661,6 @@ resolved "https://registry.yarnpkg.com/@types/d3/-/d3-3.5.43.tgz#e9b4992817e0b6c5efaa7d6e5bb2cee4d73eab58" integrity sha512-t9ZmXOcpVxywRw86YtIC54g7M9puRh8hFedRvVfHKf5YyOP6pSxA0TvpXpfseXSCInoW4P7bggTrSDiUOs4g5w== -"@types/decompress@^4.2.3": - version "4.2.3" - resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.3.tgz#98eed48af80001038aa05690b2094915f296fe65" - integrity sha512-W24e3Ycz1UZPgr1ZEDHlK4XnvOr+CpJH3qNsFeqXwwlW/9END9gxn3oJSsp7gYdiQxrXUHwUUd3xuzVz37MrZQ== - dependencies: - "@types/node" "*" - "@types/dedent@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@types/dedent/-/dedent-0.7.0.tgz#155f339ca404e6dd90b9ce46a3f78fd69ca9b050" @@ -3716,6 +3727,11 @@ resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== +"@types/extract-zip@^1.6.2": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@types/extract-zip/-/extract-zip-1.6.2.tgz#5c7eb441c41136167a42b88b64051e6260c29e86" + integrity sha1-XH60QcQRNhZ6QriLZAUeYmDCnoY= + "@types/fancy-log@^1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@types/fancy-log/-/fancy-log-1.3.1.tgz#dd94fbc8c2e2ab8ab402ca8d04bb8c34965f0696" @@ -4151,6 +4167,13 @@ dependencies: "@types/node" "*" +"@types/mdast@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" + integrity sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw== + dependencies: + "@types/unist" "*" + "@types/memoize-one@^4.1.0": version "4.1.1" resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.1.tgz#41dd138a4335b5041f7d8fc038f9d593d88b3369" @@ -4245,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" "*" @@ -4640,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== @@ -4833,7 +4856,7 @@ dependencies: "@types/undertaker-registry" "*" -"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": +"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== @@ -4865,6 +4888,13 @@ "@types/node" "*" "@types/unist" "*" +"@types/vfile-message@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5" + integrity sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw== + dependencies: + vfile-message "*" + "@types/vfile@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/vfile/-/vfile-3.0.2.tgz#19c18cd232df11ce6fa6ad80259bc86c366b09b9" @@ -5560,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== @@ -6098,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== @@ -6560,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" @@ -6577,6 +6612,11 @@ attr-accept@^1.1.3: dependencies: core-js "^2.5.0" +attr-accept@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.1.tgz#89b48de019ed4342f1865626b4389c666b3ed231" + integrity sha512-GpefLMsbH5ojNgfTW+OBin2xKzuHfyeNA+qCktzZojBhbA/lPZdCFMWdwk5ajb989Ok7ZT+EADqvW3TAFNMjhA== + autobind-decorator@^1.3.4: version "1.4.3" resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-1.4.3.tgz#4c96ffa77b10622ede24f110f5dbbf56691417d1" @@ -7343,19 +7383,19 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" - integrity sha1-ysMo977kVzDUBLaSID/LWQ4XLV4= +bl@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" + integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== dependencies: - readable-stream "^2.0.5" + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" -bl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" - integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== - dependencies: - readable-stream "^3.0.1" +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" @@ -7379,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== @@ -7717,19 +7757,6 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -7750,11 +7777,6 @@ buffer-equal@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -7779,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.2.1: +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== @@ -7950,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== @@ -8163,10 +8177,10 @@ catbox@10.x.x: hoek "5.x.x" joi "13.x.x" -ccount@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.3.tgz#f1cec43f332e2ea5a569fd46f9f5bde4e6102aff" - integrity sha512-Jt9tIBkRc9POUof7QA/VwWd+58fKkEEfI+/t1/eOlxKM7ZhrczNzMFefge7Ai+39y1pR/pP6cI19guHy3FSLmw== +ccount@^1.0.0, ccount@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" + integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== center-align@^0.1.1: version "0.1.3" @@ -8293,6 +8307,11 @@ change-emitter@^0.1.2: resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" integrity sha1-6LL+PX8at9aaMhma/5HqaTFAlRU= +character-entities-html4@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125" + integrity sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g== + character-entities-legacy@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.1.tgz#f40779df1a101872bb510a3d295e1fccf147202f" @@ -8328,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= @@ -8450,7 +8469,7 @@ chokidar@^3.2.2, chokidar@^3.4.0: optionalDependencies: fsevents "~2.1.2" -chownr@^1.0.1, chownr@^1.1.1, chownr@^1.1.2: +chownr@^1.1.1, chownr@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -8634,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" @@ -8851,6 +8880,11 @@ coffeescript@~1.10.0: resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-1.10.0.tgz#e7aa8301917ef621b35d8a39f348dcdd1db7e33e" integrity sha1-56qDAZF+9iGzXYo580jc3R234z4= +collapse-white-space@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" + integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== + collapse-white-space@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.3.tgz#4b906f670e5a963a87b76b0e1689643341b6023c" @@ -9008,7 +9042,12 @@ comma-separated-tokens@^1.0.0: dependencies: trim "0.0.1" -commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.2: +comma-separated-tokens@^1.0.1: + version "1.0.8" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" + integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== + +commander@2, commander@2.19.0, commander@^2.11.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -9023,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" @@ -9048,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== @@ -9927,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" @@ -10182,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" @@ -10276,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== @@ -10334,59 +10389,6 @@ decompress-response@^5.0.0: dependencies: mimic-response "^2.0.0" -decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" - integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== - dependencies: - file-type "^5.2.0" - is-stream "^1.1.0" - tar-stream "^1.5.2" - -decompress-tarbz2@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" - integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== - dependencies: - decompress-tar "^4.1.0" - file-type "^6.1.0" - is-stream "^1.1.0" - seek-bzip "^1.0.5" - unbzip2-stream "^1.0.9" - -decompress-targz@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" - integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== - dependencies: - decompress-tar "^4.1.1" - file-type "^5.2.0" - is-stream "^1.1.0" - -decompress-unzip@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" - integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k= - dependencies: - file-type "^3.8.0" - get-stream "^2.2.0" - pify "^2.3.0" - yauzl "^2.4.2" - -decompress@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" - integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== - dependencies: - decompress-tar "^4.0.0" - decompress-tarbz2 "^4.0.0" - decompress-targz "^4.0.0" - decompress-unzip "^4.0.1" - graceful-fs "^4.1.10" - make-dir "^1.0.0" - pify "^2.3.0" - strip-dirs "^2.0.0" - dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -10684,6 +10686,13 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +detab@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.3.tgz#33e5dd74d230501bd69985a0d2b9a3382699a130" + integrity sha512-Up8P0clUVwq0FnFjDclzZsy9PadzRn5FFxrr47tQQvMHqyiFYVbpH8oXDzWtF0Q7pYy3l+RPmtBl+BsFF6wH0A== + dependencies: + repeat-string "^1.5.4" + detect-conflict@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/detect-conflict/-/detect-conflict-1.0.1.tgz#088657a66a961c05019db7c4230883b1c6b4176e" @@ -11296,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" @@ -11381,6 +11385,11 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== +emoticon@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-3.2.0.tgz#c008ca7d7620fac742fe1bf4af8ff8fed154ae7f" + integrity sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg== + emotion-theming@^10.0.19: version "10.0.27" resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-10.0.27.tgz#1887baaec15199862c89b1b984b79806f2b9ab10" @@ -11414,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== @@ -12283,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" @@ -12333,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" @@ -12406,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" @@ -12444,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== @@ -12687,10 +12689,10 @@ extract-zip@1.7.0, extract-zip@^1.6.6, extract-zip@^1.7.0: mkdirp "^0.5.4" yauzl "^2.10.0" -extract-zip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.0.tgz#f53b71d44f4ff5a4527a2259ade000fb8b303492" - integrity sha512-i42GQ498yibjdvIhivUsRslx608whtGoFIhF26Z7O4MYncBxp8CwalOs1lnHy21A9sIohWO2+uiE4SRtC9JXDg== +extract-zip@^2.0.0, extract-zip@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== dependencies: debug "^4.1.1" get-stream "^5.1.0" @@ -12810,7 +12812,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.0" -fault@^1.0.2: +fault@^1.0.0, fault@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== @@ -12939,6 +12941,13 @@ file-saver@^1.3.8: resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8" integrity sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg== +file-selector@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0" + integrity sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ== + dependencies: + tslib "^1.9.0" + file-sync-cmp@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz#a5e7a8ffbfa493b43b923bbd4ca89a53b63b612b" @@ -12958,21 +12967,6 @@ file-type@^10.9.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-10.9.0.tgz#f6c12c7cb9e6b8aeefd6917555fd4f9eadf31891" integrity sha512-9C5qtGR/fNibHC5gzuMmmgnjH3QDDLKMa8lYe9CiZVmAnI4aUaoMh40QyUPzzs0RYo837SOBKh7TYwle4G8E4w== -file-type@^3.8.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" - integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= - -file-type@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" - integrity sha1-LdvqfHP/42No365J3DOMBYwritY= - -file-type@^6.1.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" - integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== - file-type@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-9.0.0.tgz#a68d5ad07f486414dfb2c8866f73161946714a18" @@ -13490,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" @@ -13528,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" @@ -13778,14 +13782,6 @@ get-stream@3.0.0, get-stream@^3.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= -get-stream@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" - integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4= - dependencies: - object-assign "^4.0.1" - pinkie-promise "^2.0.0" - get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -13820,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== @@ -14394,7 +14390,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4: +graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -14404,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" @@ -15094,6 +15097,31 @@ hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" +hast-to-hyperscript@^7.0.0: + version "7.0.4" + resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-7.0.4.tgz#7c4c037d9a8ea19b0a3fdb676a26448ad922353d" + integrity sha512-vmwriQ2H0RPS9ho4Kkbf3n3lY436QKLq6VaGA1pzBh36hBi3tm1DO9bR+kaJIbpT10UqaANDkMjxvjVfr+cnOA== + dependencies: + comma-separated-tokens "^1.0.0" + property-information "^5.3.0" + space-separated-tokens "^1.0.0" + style-to-object "^0.2.1" + unist-util-is "^3.0.0" + web-namespaces "^1.1.2" + +hast-to-hyperscript@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.0.tgz#768fb557765fe28749169c885056417342d71e83" + integrity sha512-NJvMYU3GlMLs7hN3CRbsNlMzusVNkYBogVWDGybsuuVQ336gFLiD+q9qtFZT2meSHzln3pNISZWTASWothMSMg== + dependencies: + "@types/unist" "^2.0.3" + comma-separated-tokens "^1.0.0" + property-information "^5.3.0" + space-separated-tokens "^1.0.0" + style-to-object "^0.3.0" + unist-util-is "^4.0.0" + web-namespaces "^1.0.0" + hast-util-from-parse5@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-5.0.0.tgz#a505a05766e0f96e389bfb0b1dd809eeefcef47b" @@ -15105,11 +15133,62 @@ hast-util-from-parse5@^5.0.0: web-namespaces "^1.1.2" xtend "^4.0.1" +hast-util-is-element@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-1.0.4.tgz#059090a05cc02e275df1ad02caf8cb422fcd2e02" + integrity sha512-NFR6ljJRvDcyPP5SbV7MyPBgF47X3BsskLnmw1U34yL+X6YC0MoBx9EyMg8Jtx4FzGH95jw8+c1VPLHaRA0wDQ== + hast-util-parse-selector@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.1.tgz#4ddbae1ae12c124e3eb91b581d2556441766f0ab" integrity sha512-Xyh0v+nHmQvrOqop2Jqd8gOdyQtE8sIP9IQf7mlVDqp924W4w/8Liuguk2L2qei9hARnQSG2m+wAOCxM7npJVw== +hast-util-raw@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-5.0.2.tgz#62288f311ec2f35e066a30d5e0277f963ad43a67" + integrity sha512-3ReYQcIHmzSgMq8UrDZHFL0oGlbuVGdLKs8s/Fe8BfHFAyZDrdv1fy/AGn+Fim8ZuvAHcJ61NQhVMtyfHviT/g== + dependencies: + hast-util-from-parse5 "^5.0.0" + hast-util-to-parse5 "^5.0.0" + html-void-elements "^1.0.0" + parse5 "^5.0.0" + unist-util-position "^3.0.0" + web-namespaces "^1.0.0" + xtend "^4.0.0" + zwitch "^1.0.0" + +hast-util-to-html@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-6.1.0.tgz#86bcd19c3bd46af456984f8f34db16298c2b10b0" + integrity sha512-IlC+LG2HGv0Y8js3wqdhg9O2sO4iVpRDbHOPwXd7qgeagpGsnY49i8yyazwqS35RA35WCzrBQE/n0M6GG/ewxA== + dependencies: + ccount "^1.0.0" + comma-separated-tokens "^1.0.1" + hast-util-is-element "^1.0.0" + hast-util-whitespace "^1.0.0" + html-void-elements "^1.0.0" + property-information "^5.2.0" + space-separated-tokens "^1.0.0" + stringify-entities "^2.0.0" + unist-util-is "^3.0.0" + xtend "^4.0.1" + +hast-util-to-parse5@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-5.1.2.tgz#09d27bee9ba9348ea05a6cfcc44e02f9083969b6" + integrity sha512-ZgYLJu9lYknMfsBY0rBV4TJn2xiwF1fXFFjbP6EE7S0s5mS8LIKBVWzhA1MeIs1SWW6GnnE4In6c3kPb+CWhog== + dependencies: + hast-to-hyperscript "^7.0.0" + property-information "^5.0.0" + web-namespaces "^1.0.0" + xtend "^4.0.0" + zwitch "^1.0.0" + +hast-util-whitespace@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41" + integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A== + hastscript@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.0.0.tgz#fee10382c1bc4ba3f1be311521d368c047d2c43a" @@ -15162,6 +15241,11 @@ highlight.js@^9.12.0, highlight.js@~9.12.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" integrity sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4= +highlight.js@~10.1.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.1.2.tgz#c20db951ba1c22c055010648dfffd7b2a968e00c" + integrity sha512-Q39v/Mn5mfBlMff9r+zzA+gWxRsCRKwEMvYTiisLr/XUiFI/4puWt0Ojdko3R3JCNWGdOWaA5g/Yxqa23kC5AA== + highlight.js@~9.13.0: version "9.13.1" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e" @@ -15311,6 +15395,11 @@ html-to-react@^1.3.4: lodash.camelcase "^4.3.0" ramda "^0.26" +html-void-elements@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" + integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w== + html-webpack-plugin@^4.0.0-beta.2: version "4.0.0-beta.5" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.5.tgz#2c53083c1151bfec20479b1f8aaf0039e77b5513" @@ -15830,6 +15919,11 @@ ini@^1.2.0, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== + inline-style-prefixer@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911" @@ -16237,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" @@ -16251,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" @@ -16280,6 +16374,11 @@ is-decimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.1.tgz#f5fb6a94996ad9e8e3761fbfbd091f1fca8c4e82" integrity sha1-9ftqlJlq2ejjdh+/vQkfH8qMToI= +is-decimal@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" + integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -16412,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" @@ -16428,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" @@ -16483,11 +16582,6 @@ is-native@^1.0.1: is-nil "^1.0.0" to-source-code "^1.0.0" -is-natural-number@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" - integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= - is-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" @@ -16612,6 +16706,11 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-object@2.0.4, is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -17935,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" @@ -18099,10 +18207,10 @@ kdbush@^3.0.0: resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0" integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew== -kea@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/kea/-/kea-2.1.1.tgz#6e3c65c4873b67d270a2ec7bf73b0d178937234c" - integrity sha512-W9o4lHLOcEDIu3ASHPrWJJJzL1bMkGyxaHn9kuaDgI96ztBShVrf52R0QPGlQ2k9ca3XnkB/dnVHio1UB8kGWA== +kea@2.2.0-rc.4: + version "2.2.0-rc.4" + resolved "https://registry.yarnpkg.com/kea/-/kea-2.2.0-rc.4.tgz#cc0376950530a6751f73387c4b25a39efa1faa77" + integrity sha512-pYuwaCiJkBvHZShi8kqhk8dC4DjeELdK51Lw7Pn0tNdJgZJDF6COhsUiF/yrh9d7woNYDxKfuxH+QWZFfo8PkA== kew@~0.1.7: version "0.1.7" @@ -18236,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= @@ -18469,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== @@ -18898,6 +19006,11 @@ lodash.throttle@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= +lodash.toarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" + integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= + lodash.unescape@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" @@ -18913,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== @@ -19087,6 +19200,14 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lowlight@^1.2.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.14.0.tgz#83ebc143fec0f9e6c0d3deffe01be129ce56b108" + integrity sha512-N2E7zTM7r1CwbzwspPxJvmjAbxljCPThTFawEX2Z7+P3NGrrvY54u8kyU16IY4qWfoVIxY8SYCS8jTkuG7TqYA== + dependencies: + fault "^1.0.0" + highlight.js "~10.1.0" + lowlight@~1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc" @@ -19375,6 +19496,30 @@ mdast-add-list-metadata@1.0.1: dependencies: unist-util-visit-parents "1.1.2" +mdast-util-definitions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-3.0.1.tgz#06af6c49865fc63d6d7d30125569e2f7ae3d0a86" + integrity sha512-BAv2iUm/e6IK/b2/t+Fx69EL/AGcq/IG2S+HxHjDJGfLJtd6i9SZUS76aC9cig+IEucsqxKTR0ot3m933R3iuA== + dependencies: + unist-util-visit "^2.0.0" + +mdast-util-to-hast@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-9.1.0.tgz#6ef121dd3cd3b006bf8650b1b9454da0faf79ffe" + integrity sha512-Akl2Vi9y9cSdr19/Dfu58PVwifPXuFt1IrHe7l+Crme1KvgUT+5z+cHLVcQVGCiNTZZcdqjnuv9vPkGsqWytWA== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.3" + collapse-white-space "^1.0.0" + detab "^2.0.0" + mdast-util-definitions "^3.0.0" + mdurl "^1.0.0" + trim-lines "^1.0.0" + unist-builder "^2.0.0" + unist-util-generated "^1.0.0" + unist-util-position "^3.0.0" + unist-util-visit "^2.0.0" + mdn-data@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" @@ -19385,7 +19530,7 @@ mdn-data@2.0.6: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== -mdurl@^1.0.1: +mdurl@^1.0.0, mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= @@ -19812,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== @@ -19900,6 +20045,11 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -20074,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" @@ -20438,6 +20588,13 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" +node-emoji@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" + integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw== + dependencies: + lodash.toarray "^4.4.0" + node-environment-flags@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" @@ -20474,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" @@ -21469,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= @@ -22528,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" @@ -22737,6 +22889,13 @@ property-information@^5.0.0, property-information@^5.0.1: dependencies: xtend "^4.0.1" +property-information@^5.2.0, property-information@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.5.0.tgz#4dc075d493061a82e2b7d096f406e076ed859943" + integrity sha512-RgEbCx2HLa1chNgvChcx+rrCWD0ctBmGSE0M7lVm1yyv4UbvbrWoXp/BkVLZefzjrRBGW8/Js6uh/BnlHXFyjA== + dependencies: + xtend "^4.0.0" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -22823,14 +22982,6 @@ puid@1.0.7: resolved "https://registry.yarnpkg.com/puid/-/puid-1.0.7.tgz#fa638a737d7b20419059d93965aed36ca20e1c84" integrity sha1-+mOKc317IEGQWdk5Za7TbKIOHIQ= -pump@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" - integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -23031,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" @@ -23374,6 +23525,15 @@ react-draggable@^4.0.3: classnames "^2.2.5" prop-types "^15.6.0" +react-dropzone@^10.2.1: + version "10.2.2" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.2.2.tgz#67b4db7459589a42c3b891a82eaf9ade7650b815" + integrity sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA== + dependencies: + attr-accept "^2.0.0" + file-selector "^0.1.12" + prop-types "^15.7.2" + react-dropzone@^4.2.9: version "4.3.0" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-4.3.0.tgz#facdd7db16509772633c9f5200621ac01aa6706f" @@ -23412,14 +23572,14 @@ react-focus-lock@^2.1.0, react-focus-lock@^2.3.1: use-callback-ref "^1.2.1" use-sidecar "^1.0.1" -react-focus-on@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.4.1.tgz#e184f3c44185e341598c5d9d44b2987ad459b240" - integrity sha512-KGRIl0iAu+1k1dcX7eQCXF5ZR/nl+XyXN5Ukw/OY80vLaK2b6vDzNqnX0HdYbY5xSUhIRUvMWEzSsdEyPjvk/Q== +react-focus-on@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.5.0.tgz#3cdebbefee26a083976e1700ba75f7377040f7f1" + integrity sha512-RqGAHOxhRAaMSVHIN5IpY7YL6AJkD/DMa/+iPDV7aB6XWRQfg3v2q35egIZgMWP2xhXaRVai3B80dpVWyj4Rcw== dependencies: aria-hidden "^1.1.1" react-focus-lock "^2.3.1" - react-remove-scroll "^2.3.0" + react-remove-scroll "^2.4.0" react-style-singleton "^2.1.0" use-callback-ref "^1.2.3" use-sidecar "^1.0.1" @@ -23632,10 +23792,10 @@ react-remove-scroll-bar@^2.1.0: react-style-singleton "^2.1.0" tslib "^1.0.0" -react-remove-scroll@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.3.0.tgz#3af06fe2f7130500704b676cdef94452c08fe593" - integrity sha512-UqVimLeAe+5EHXKfsca081hAkzg3WuDmoT9cayjBegd6UZVhlTEchleNp9J4TMGkb/ftLve7ARB5Wph+HJ7A5g== +react-remove-scroll@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.4.0.tgz#190c16eb508c5927595935499e8f5dd9ab0e75cf" + integrity sha512-BZIO3GaEs0Or1OhA5C//n1ibUP1HdjJmqUVUsOCMxwoIpaCocbB9TFKwHOkBa/nyYy3slirqXeiPYGwdSDiseA== dependencies: react-remove-scroll-bar "^2.1.0" react-style-singleton "^2.1.0" @@ -24064,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.0, 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== @@ -24087,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.1, 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== @@ -24569,6 +24707,29 @@ rehype-parse@^6.0.0: parse5 "^5.0.0" xtend "^4.0.1" +rehype-raw@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-4.0.2.tgz#5d3191689df96c8c651ce5f51a6c668d2c07b9c8" + integrity sha512-xQt94oXfDaO7sK9mJBtsZXkjW/jm6kArCoYN+HqKZ51O19AFHlp3Xa5UfZZ2tJkbpAZzKtgVUYvnconk9IsFuA== + dependencies: + hast-util-raw "^5.0.0" + +rehype-react@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.1.0.tgz#95f8c936eea2159f92adfbf58e5e90be86a97cbf" + integrity sha512-hQ4DSGOJKA1a87Ei4fJtSHzopbfgoHkwjWMCFpLrcVR5+AIyCOtHy4oQcpGF11kTZOU6oKmJ9UKzO/JpI/XZWA== + dependencies: + "@mapbox/hast-util-table-cell-style" "^0.1.3" + hast-to-hyperscript "^9.0.0" + +rehype-stringify@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-6.0.1.tgz#b6aa9f84d5276c5d247c62fc1ea15983a35a4575" + integrity sha512-JfEPRDD4DiG7jet4md7sY07v6ACeb2x+9HWQtRPm2iA6/ic31hCv1SNBUtpolJASxQ/D8gicXiviW4TJKEMPKQ== + dependencies: + hast-util-to-html "^6.0.0" + xtend "^4.0.0" + relateurl@0.2.x: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -24593,6 +24754,23 @@ release-zalgo@^1.0.0: dependencies: es6-error "^4.0.1" +remark-emoji@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-2.1.0.tgz#69165d1181b98a54ad5d9ef811003d53d7ebc7db" + integrity sha512-lDddGsxXURV01WS9WAiS9rO/cedO1pvr9tahtLhr6qCGFhHG4yZSJW3Ha4Nw9Uk1hLNmUBtPC0+m45Ms+xEitg== + dependencies: + emoticon "^3.2.0" + node-emoji "^1.10.0" + unist-util-visit "^2.0.2" + +remark-highlight.js@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/remark-highlight.js/-/remark-highlight.js-5.2.0.tgz#6d8d22085e0c76573744b7e3706fc232269f5b02" + integrity sha512-5tCr1CfdXDYzR8HCAnohlr1rK8DjUTFxEZdr+QEul5o13+EOEt5RrO8U6Znf8Faj5rVLcMJtxLPq6hHrZFo33A== + dependencies: + lowlight "^1.2.0" + unist-util-visit "^1.0.0" + remark-parse@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-5.0.0.tgz#4c077f9e499044d1d5c13f80d7a98cf7b9285d95" @@ -24614,6 +24792,34 @@ remark-parse@^5.0.0: vfile-location "^2.0.0" xtend "^4.0.1" +remark-parse@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-7.0.2.tgz#41e7170d9c1d96c3d32cf1109600a9ed50dba7cf" + integrity sha512-9+my0lQS80IQkYXsMA8Sg6m9QfXYJBnXjWYN5U+kFc5/n69t+XZVXU/ZBYr3cYH8FheEGf1v87rkFDhJ8bVgMA== + dependencies: + collapse-white-space "^1.0.2" + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + is-word-character "^1.0.0" + markdown-escapes "^1.0.0" + parse-entities "^1.1.0" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + trim "0.0.1" + trim-trailing-lines "^1.0.0" + unherit "^1.0.4" + unist-util-remove-position "^1.0.0" + vfile-location "^2.0.0" + xtend "^4.0.1" + +remark-rehype@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-7.0.0.tgz#8e106e49806c69b2e9523b76d24965119e2da67b" + integrity sha512-uqQ/VbaTdxyu/da6npHAso6hA00cMqhA3a59RziQdOLN2KEIkPykAVy52IcmZEVTuauXO0VtpxkyCey4phtHzQ== + dependencies: + mdast-util-to-hast "^9.1.0" + remedial@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/remedial/-/remedial-1.0.8.tgz#a5e4fd52a0e4956adbaf62da63a5a46a78c578a0" @@ -24700,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= @@ -25446,13 +25652,6 @@ seedrandom@^3.0.5: resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -seek-bzip@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" - integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== - dependencies: - commander "^2.8.1" - select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -26736,6 +26935,17 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-2.0.0.tgz#fa7ca6614b355fb6c28448140a20c4ede7462827" + integrity sha512-fqqhZzXyAM6pGD9lky/GOPq6V4X0SeTAFBl0iXb/BzOegl40gpf/bV3QQP7zULNYvjr6+Dx8SCaDULjVoOru0A== + dependencies: + character-entities-html4 "^1.0.0" + character-entities-legacy "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.2" + is-hexadecimal "^1.0.0" + stringify-object@3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.2.tgz#9853052e5a88fb605a44cd27445aa257ad7ffbcd" @@ -26824,13 +27034,6 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== -strip-dirs@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" - integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== - dependencies: - is-natural-number "^4.0.1" - strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -26912,6 +27115,20 @@ style-loader@^1.1.3: loader-utils "^1.2.3" schema-utils "^2.6.4" +style-to-object@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.2.3.tgz#afcf42bc03846b1e311880c55632a26ad2780bcb" + integrity sha512-1d/k4EY2N7jVLOqf2j04dTc37TPOv/hHxZmvpg8Pdh8UYydxeu/C1W1U4vD8alzf5V2Gt7rLsmkr4dxAlDm9ng== + dependencies: + inline-style-parser "0.1.1" + +style-to-object@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" + integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== + dependencies: + inline-style-parser "0.1.1" + styled-components@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.0.tgz#2e3985b54f461027e1c91af3229e1c2530872a4e" @@ -27019,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" @@ -27064,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" @@ -27286,45 +27503,22 @@ tape@^5.0.1: string.prototype.trim "^1.2.1" through "^2.3.8" -tar-fs@^1.16.3: - version "1.16.3" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" - integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw== - dependencies: - chownr "^1.0.1" - mkdirp "^0.5.1" - pump "^1.0.0" - tar-stream "^1.1.2" - -tar-stream@^1.1.2: - version "1.5.5" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.5.tgz#5cad84779f45c83b1f2508d96b09d88c7218af55" - integrity sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg== - dependencies: - bl "^1.0.0" - end-of-stream "^1.0.0" - readable-stream "^2.0.0" - xtend "^4.0.0" - -tar-stream@^1.5.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" - integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== +tar-fs@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" + integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== dependencies: - bl "^1.0.0" - buffer-alloc "^1.2.0" - end-of-stream "^1.0.0" - fs-constants "^1.0.0" - readable-stream "^2.3.0" - to-buffer "^1.1.1" - xtend "^4.0.0" + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" -tar-stream@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" - integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== +tar-stream@^2.0.0, tar-stream@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" + integrity sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA== dependencies: - bl "^3.0.0" + bl "^4.0.1" end-of-stream "^1.4.1" fs-constants "^1.0.0" inherits "^2.0.3" @@ -27730,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" @@ -27748,11 +27949,6 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= -to-buffer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" - integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== - to-camel-case@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46" @@ -27944,6 +28140,11 @@ treeify@^1.0.1, treeify@^1.1.0: resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== +trim-lines@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-1.1.3.tgz#839514be82428fd9e7ec89e35081afe8f6f93115" + integrity sha512-E0ZosSWYK2mkSu+KEtQ9/KqarVjA9HztOSX+9FDdNacRAq29RRV6ZQNgob3iuW8Htar9vAfEa6yyt5qBAHZDBA== + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -28244,13 +28445,6 @@ typescript@4.0.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, types resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== -typings-tester@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/typings-tester/-/typings-tester-0.3.2.tgz#04cc499d15ab1d8b2d14dd48415a13d01333bc5b" - integrity sha512-HjGoAM2UoGhmSKKy23TYEKkxlphdJFdix5VvqWFLzH1BJVnnwG38tpC6SXPgqhfFGfHY77RlN1K8ts0dbWBQ7A== - dependencies: - commander "^2.12.2" - ua-parser-js@^0.7.18: version "0.7.21" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" @@ -28301,14 +28495,6 @@ uid-safe@2.1.5: dependencies: random-bytes "~1.0.0" -unbzip2-stream@^1.0.9: - version "1.4.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" - integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== - dependencies: - buffer "^5.2.1" - through "^2.3.8" - unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -28467,6 +28653,17 @@ unified@^7.1.0: vfile "^3.0.0" x-is-string "^0.1.0" +unified@^8.4.2: + version "8.4.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-8.4.2.tgz#13ad58b4a437faa2751a4a4c6a16f680c500fff1" + integrity sha512-JCrmN13jI4+h9UAyKEoGcDZV+i1E7BLFuG7OsaDvTXI5P0qhHX+vZO/kOhz9jn8HGENDKbwSeB0nVOg4gVStGA== + dependencies: + bail "^1.0.0" + extend "^3.0.0" + is-plain-obj "^2.0.0" + trough "^1.0.0" + vfile "^4.0.0" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -28518,11 +28715,36 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +unist-builder@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" + integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== + +unist-util-generated@^1.0.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.5.tgz#1e903e68467931ebfaea386dae9ea253628acd42" + integrity sha512-1TC+NxQa4N9pNdayCYA1EGUOCAO0Le3fVp7Jzns6lnua/mYgwHo0tz5WUAfrdpNch1RZLHc61VZ1SDgrtNXLSw== + unist-util-is@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b" integrity sha1-DDEmKeP5YMZukx6BLT2A53AQlHs= +unist-util-is@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd" + integrity sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A== + +unist-util-is@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.2.tgz#c7d1341188aa9ce5b3cff538958de9895f14a5de" + integrity sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ== + +unist-util-position@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" + integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== + unist-util-remove-position@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.1.tgz#5a85c1555fc1ba0c101b86707d15e50fa4c871bb" @@ -28547,6 +28769,28 @@ unist-util-visit-parents@1.1.2: resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz#f6e3afee8bdbf961c0e6f028ea3c0480028c3d06" integrity sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q== +unist-util-visit-parents@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz#25e43e55312166f3348cae6743588781d112c1e9" + integrity sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g== + dependencies: + unist-util-is "^3.0.0" + +unist-util-visit-parents@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.0.tgz#4dd262fb9dcfe44f297d53e882fc6ff3421173d5" + integrity sha512-0g4wbluTF93npyPrp/ymd3tCDTMnP0yo2akFD2FIBAYXq/Sga3lwaU1D8OYKbtpioaI6CkDcQ6fsMnmtzt7htw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^4.0.0" + +unist-util-visit@^1.0.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3" + integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw== + dependencies: + unist-util-visit-parents "^2.0.0" + unist-util-visit@^1.1.0, unist-util-visit@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.3.0.tgz#41ca7c82981fd1ce6c762aac397fc24e35711444" @@ -28554,6 +28798,15 @@ unist-util-visit@^1.1.0, unist-util-visit@^1.3.0: dependencies: unist-util-is "^2.1.1" +unist-util-visit@^2.0.0, unist-util-visit@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" + integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^4.0.0" + unist-util-visit-parents "^3.0.0" + universal-user-agent@^2.0.0, universal-user-agent@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.0.3.tgz#9f6f09f9cc33de867bb720d84c08069b14937c6c" @@ -28566,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" @@ -28606,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" @@ -28623,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" @@ -28770,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= @@ -29327,6 +29585,14 @@ vfile-location@^2.0.0: resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.2.tgz#d3675c59c877498e492b4756ff65e4af1a752255" integrity sha1-02dcWch3SY5JK0dW/2Xkrxp1IlU= +vfile-message@*: + version "2.0.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" + integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^2.0.0" + vfile-message@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-1.0.0.tgz#a6adb0474ea400fa25d929f1d673abea6a17e359" @@ -29395,6 +29661,17 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +vfile@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.0.tgz#26c78ac92eb70816b01d4565e003b7e65a2a0e01" + integrity sha512-a/alcwCvtuc8OX92rqqo7PflxiCgXRFjdyoGVuYV+qbgCb0GgZJRvIgCD4+U/Kl1yhaRsaTwksF88xbPyGsgpw== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + replace-ext "1.0.0" + unist-util-stringify-position "^2.0.0" + vfile-message "^2.0.0" + vinyl-file@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a" @@ -29640,6 +29917,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-namespaces@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" + integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== + web-namespaces@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.2.tgz#c8dc267ab639505276bae19e129dbd6ae72b22b4" @@ -30598,7 +30880,7 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yauzl@2.10.0, yauzl@^2.10.0, yauzl@^2.4.2: +yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= @@ -30814,3 +31096,8 @@ zlib@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/zlib/-/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0" integrity sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA= + +zwitch@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" + integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==