diff --git a/.ci/run.sh b/.ci/run.sh index f0a18d929b51..88ce0bd9986a 100755 --- a/.ci/run.sh +++ b/.ci/run.sh @@ -5,6 +5,7 @@ set -e # move to Kibana root cd "$(dirname "$0")/.." +source src/dev/ci_setup/load_env_keys.sh source src/dev/ci_setup/extract_bootstrap_cache.sh source src/dev/ci_setup/setup.sh source src/dev/ci_setup/checkout_sibling_es.sh diff --git a/.eslintignore b/.eslintignore index 9eacf2fd47d7..12a3f6848604 100644 --- a/.eslintignore +++ b/.eslintignore @@ -25,6 +25,8 @@ bower_components /packages/kbn-ui-framework/dist /packages/kbn-ui-framework/doc_site/build /packages/kbn-ui-framework/generator-kui/*/templates/ +/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/ +/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/ /x-pack/legacy/plugins/maps/public/vendor/** /x-pack/coverage /x-pack/build diff --git a/.eslintrc.js b/.eslintrc.js index 019f93d91b55..c821bfd5a240 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -186,8 +186,10 @@ module.exports = { 'x-pack/legacy/plugins/apm/**/*.js', 'test/*/config.ts', 'test/visual_regression/tests/**/*', - 'x-pack/test/visual_regression/tests/**/*', - 'x-pack/test/*/config.ts', + 'x-pack/test/*/{tests,test_suites,apis,apps}/**/*', + 'x-pack/test/*/*config.*ts', + 'x-pack/test/saved_object_api_integration/*/apis/**/*', + 'x-pack/test/ui_capabilities/*/tests/**/*', ], rules: { 'import/no-default-export': 'off', @@ -396,6 +398,14 @@ module.exports = { 'no-console': ['warn', { allow: ['error'] }], }, }, + { + plugins: ['react-hooks'], + files: ['x-pack/legacy/plugins/apm/**/*.{ts,tsx}'], + rules: { + 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks + 'react-hooks/exhaustive-deps': ['error', { additionalHooks: '^useFetcher$' }], + }, + }, /** * GIS overrides diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 274d9a25ef53..3abc37a5f60b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -36,6 +36,7 @@ /src/core/ @elastic/kibana-platform /src/legacy/server/saved_objects/ @elastic/kibana-platform /src/legacy/ui/public/saved_objects @elastic/kibana-platform +/config/kibana.yml @elastic/kibana-platform # Security /x-pack/legacy/plugins/security/ @elastic/kibana-security @@ -57,6 +58,7 @@ # Elasticsearch UI /src/legacy/core_plugins/console/ @elastic/es-ui +/src/plugins/es_ui_shared/ @elastic/es-ui /x-pack/legacy/plugins/console_extensions/ @elastic/es-ui /x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui /x-pack/legacy/plugins/index_lifecycle_management/ @elastic/es-ui diff --git a/.i18nrc.json b/.i18nrc.json index bcc1fe70ab1e..292f4d7e9c10 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -15,16 +15,17 @@ "embeddableApi": "src/legacy/core_plugins/embeddable_api", "kbnVislibVisTypes": "src/legacy/core_plugins/kbn_vislib_vis_types", "visTypeMarkdown": "src/legacy/core_plugins/vis_type_markdown", - "metricVis": "src/legacy/core_plugins/metric_vis", + "visTypeMetric": "src/legacy/core_plugins/vis_type_metric", "visTypeVega": "src/legacy/core_plugins/vis_type_vega", - "tableVis": "src/legacy/core_plugins/table_vis", + "visTypeTable": "src/legacy/core_plugins/vis_type_table", "regionMap": "src/legacy/core_plugins/region_map", "statusPage": "src/legacy/core_plugins/status_page", "tileMap": "src/legacy/core_plugins/tile_map", "timelion": "src/legacy/core_plugins/timelion", - "tagCloud": "src/legacy/core_plugins/tagcloud", + "visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud", "tsvb": "src/legacy/core_plugins/metrics", - "kbnESQuery": "packages/kbn-es-query" + "kbnESQuery": "packages/kbn-es-query", + "inspector": "src/plugins/inspector" }, "exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"], "translations": [] diff --git a/NOTICE.txt b/NOTICE.txt index d1903a471341..4780881be694 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -107,6 +107,39 @@ 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. +--- +This product includes code that is adapted from mapbox-gl-js, which is +available under a "BSD-3-Clause" license. +https://github.com/mapbox/mapbox-gl-js/blob/master/src/util/image.js + +Copyright (c) 2016, Mapbox + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Mapbox GL JS nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + --- This product includes code that is based on facebookincubator/idx, which was available under a "MIT" license. diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 8cc7f64adc3a..df4b17e90577 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -1209,17 +1209,23 @@ Aliases: `label`, `text`, `description` Default: `""` +|`labelFont` +|`style` +|The CSS font properties for the label. For example, `font-family` or `font-weight`. + +Default: `{font size=14 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}`. + |`metricFont` |`style` |The CSS font properties for the metric. For example, `font-family` or `font-weight`. Default: `{font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center lHeight=48}`. -|`labelFont` -|`style` -|The CSS font properties for the label. For example, `font-family` or `font-weight`. +|`metricFormat` -Default: `{font size=14 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}`. +Alias: `format` +|`string` +|A NumeralJS format string. For example, `"0.0a"` or `"0%"`. See http://numeraljs.com/#format. |=== *Returns:* `render` diff --git a/docs/canvas/canvas-getting-started.asciidoc b/docs/canvas/canvas-getting-started.asciidoc index c02c6b1f3e21..3874b91b85e9 100644 --- a/docs/canvas/canvas-getting-started.asciidoc +++ b/docs/canvas/canvas-getting-started.asciidoc @@ -7,7 +7,7 @@ To get up and running with Canvas, use the following tutorial where you'll creat [float] === Before you begin -For this tutorial, you'll need to add the {kibana-ref}/add-sample-data.html[Sample eCommerce orders data]. +For this tutorial, you'll need to add the <>. [float] === Create and personalize your workpad diff --git a/docs/canvas/canvas-workpad.asciidoc b/docs/canvas/canvas-workpad.asciidoc index 1b47beb01dc3..87df4ddb7537 100644 --- a/docs/canvas/canvas-workpad.asciidoc +++ b/docs/canvas/canvas-workpad.asciidoc @@ -14,7 +14,7 @@ When you create a workpad, you'll start with a blank page, or you can choose a w * To import an existing workpad, click and drag a workpad JSON file to the *Import workpad JSON file* field. -For advanced workpad examples, add a {kibana-ref}/add-sample-data.html[sample Kibana data set], then select *Canvas* from the *View Data* dropdown list. +For advanced workpad examples, add a <>, then select *Canvas* from the *View Data* dropdown list. For more workpad inspiration, go to the link:https://www.elastic.co/blog/[Elastic Blog]. diff --git a/docs/code/code-basic-nav.asciidoc b/docs/code/code-basic-nav.asciidoc index 6b7479051425..77a56b24801c 100644 --- a/docs/code/code-basic-nav.asciidoc +++ b/docs/code/code-basic-nav.asciidoc @@ -17,4 +17,9 @@ Clicking *Blame* shows the most recent commit per line. [role="screenshot"] image::images/code-blame.png[] +[float] +==== Branch selector +You can use the Branch selector to view different branches of a repo. Note that code intelligence and search index are not available for any branch other than master branch. + + include::code-semantic-nav.asciidoc[] diff --git a/docs/code/code-install-lang-server.asciidoc b/docs/code/code-install-lang-server.asciidoc index acc6c5a9347e..1bb5a24f0c7f 100644 --- a/docs/code/code-install-lang-server.asciidoc +++ b/docs/code/code-install-lang-server.asciidoc @@ -16,6 +16,10 @@ You can check the status of the language servers and get installation instructio [role="screenshot"] image::images/code-lang-server-status.png[] +[float] +=== Ctag language server +*Code* also uses a Ctag language server to generate symbol information and code intelligence when a dedicated language server is not available. The code intelligence information generated by the Ctag language server is less accurate but covers more languages. + include::code-basic-nav.asciidoc[] diff --git a/docs/code/code-repo-management.asciidoc b/docs/code/code-repo-management.asciidoc index 0424242f760a..dde3c4dbc6fd 100644 --- a/docs/code/code-repo-management.asciidoc +++ b/docs/code/code-repo-management.asciidoc @@ -15,12 +15,7 @@ Deleting a repo removes it from local storage and the Elasticsearch index. [float] ==== Reindex a repo -You can set *Code* to automatically reindex imported repos at set intervals by set the following config in `kibana.yaml`. - -[source,yaml] ----- -xpack.code.disableIndexScheduler: false ----- +*Code* automatically reindexes an imported repo at set intervals, but in some cases you might need to manually refresh the index. For example, you might refresh an index after a new language server is installed. Or, you might want to immediately update the index to the HEAD revision. Click *Reindex* to initiate a reindex. In some cases you might need to manually refresh the index besides automatic indexing. For example, you might refresh an index after a new language server is installed. Or, you might want to immediately update the index to the HEAD revision. Click *Reindex* to initiate a reindex. diff --git a/docs/dashboard.asciidoc b/docs/dashboard.asciidoc index 48189465bc3e..2b7fd8052f96 100644 --- a/docs/dashboard.asciidoc +++ b/docs/dashboard.asciidoc @@ -3,66 +3,84 @@ [partintro] -- -A Kibana _dashboard_ displays a collection of visualizations, searches, and {kibana-ref}/maps.html[maps]. -You can arrange, resize, and edit the dashboard content and then save the dashboard -so you can share it. -[role="screenshot"] -image:images/Dashboard_example.png[Example dashboard] +A {kib} _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. --- +To start working with dashboards, click *Dashboard* in the side navigation. +With *Dashboard*, you can: -[[dashboard-getting-started]] -== Building a Dashboard +* <> +* <> +* <> +* <> +* <> + + +[role="screenshot"] +image:images/Dashboard_example.png[Example dashboard] -If you haven't yet indexed data into {es} or created an index pattern, -you'll be prompted to do so as you follow the steps for creating a dashboard. -Or, you can use one of the prebuilt sample data sets, available from the -Kibana home page. [float] [[dashboard-read-only-access]] === [xpack]#Read only access# -When you have insufficient privileges to create or save dashboards, the following -indicator in Kibana will be displayed. The buttons to create new dashboards or edit -existing dashboard won't be visible. For more information on granting access to -Kibana see <>. +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's read only access indicator in Kibana's header] [float] +[[dashboard-getting-started]] +=== Interact with dashboards + +When you open *Dashhboard*, you're presented an overview of your dashboards. +If you don't have any dashboards, you can add +<>, +which include pre-built dashboards. + +Once you open a dashboard, you can filter the data +by entering a search query, changing the time filter, or clicking +in the visualizations, searches, and maps. If a dashboard element has a stored query, +both queries are applied. + +-- + [[dashboard-create-new-dashboard]] -=== Creating a 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. -. In the side navigation, click *Dashboard*. +For an end-to-end example, see <>. + +. Open *Dashboard.* . Click *Create new dashboard.* . Click *Add*. -. [[adding-visualizations-to-a-dashboard]]Use *Add Panels* to add visualizations -and saved searches to the dashboard. If you have a large number of -visualizations, you can filter the lists. +. Use *Add panels* to add elements to the dashboard. ++ +The visualizations, saved searches, and maps +are stored in panels that you can move and resize. A +menu in the upper right of the panel has options for customizing +the panel. You can add elements from +multiple indices, and the same element can appear in multiple dashboards. + [role="screenshot"] image:images/Dashboard_add_visualization.png[Example add visualization to dashboard] -. [[saving-dashboards]]When you're finished adding and arranging the panels, -go to the menu bar and click *Save*. - -. In *Save Dashboard*, enter a dashboard title and optionally a description. - -. To store the time period specified in the time filter, enable *Store time -with dashboard*. - -. Click *Save*. - -[[loading-a-saved-dashboard]] -To import, export, and delete dashboards, see <>. +. When you're finished adding and arranging the panels, +*Save* the dashboard. +[float] [[customizing-your-dashboard]] -== Arranging Dashboard Elements +=== Arrange dashboard elements -The visualizations and searches in a dashboard are stored in panels that you can move, -resize, edit, and delete. To start editing, click *Edit* in the menu bar. +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. @@ -71,53 +89,59 @@ resize, edit, and delete. To start editing, click *Edit* in the menu bar. * To resize a panel, click the resize control on the lower right and drag to the new dimensions. -[[removing-containers]] -Additional commands for managing the panel and its contents -are in the gear menu in the upper right. - -[role="screenshot"] -image:images/Dashboard_Resize_Menu.png[Example dashboard] +* To toggle the use of margins and panel titles, use the *Options* menu in the upper left. -NOTE: Deleting a panel from a +* 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. -[[viewing-detailed-information]] -== Inspecting a Visualization from the Dashboard -Many visualizations allow you to inspect the data and requests behind the -visualization. +[float] +[[sharing-dashboards]] +=== Share a dashboard + +[[embedding-dashboards]] +When you've finished your dashboard, you can share it with your teammates. +From the *Share* menu, you can: + +* Embed the code in a web page. Users must have Kibana access +to view an embedded dashboard. +* Share a direct link to a {kib} dashboard +* Generate a PDF report +* Generate a PNG report + +TIP: You can create a link to a dashboard by title by doing this: + +`${domain}/${basepath?}/app/kibana#/dashboards?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]] +=== Import and export dashboards -In the dashboard, expand the visualization's panel menu (or gear menu if in -*Edit* mode) and select *Inspect*. +To import and export dashboards, go to *Management > Saved Objects*. For details, +see <>. -The initial view shows the underlying data for the visualization. To view the -requests that were made for the visualization, choose *Requests* from the *View* -menu. +[float] +[[viewing-detailed-information]] +=== Inspect and edit elements -The views you'll see depend on the element that you inspect. +Many dashboard elements allow you to drill down into the data and requests +behind the element. Open the menu in the upper right of the panel and select *Inspect*. +The views you see depend on the element that you inspect. [role="screenshot"] -image:images/Dashboard_visualization_data.png[Example of visualization data] +image:images/Dashboard_inspect.png[Inspect in dashboard] +To open an element for editing, put the dashboard in *Edit* mode, +and then select *Edit* from the panel menu. The changes you make appear in +every dashboard that uses the element. +include::management/dashboard_only_mode/index.asciidoc[] -[[sharing-dashboards]] -== Sharing a Dashboard -You can either share a direct link to a Kibana dashboard, -or embed the dashboard in a web page. Users must have Kibana access -to view an embedded dashboard. -[[embedding-dashboards]] -. Open the dashboard you want to share. -. In the menu bar, click *Share*. -. Copy the link you want to share or the iframe you want to embed. You can -share the live dashboard or a static snapshot of the current point in time. -TIP: You can create a link to a dashboard by title by doing this: + -`${domain}/${basepath?}/app/kibana#/dashboards?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}. diff --git a/docs/development/core/development-basepath.asciidoc b/docs/development/core/development-basepath.asciidoc index e84171c79cb2..d49dfe2938fa 100644 --- a/docs/development/core/development-basepath.asciidoc +++ b/docs/development/core/development-basepath.asciidoc @@ -66,6 +66,15 @@ To accomplish this the `serve` task does a few things: - redirects from `/{any}/app/{appName}` to `/{randomBasePath}/app/{appName}` so that refreshes should work - proxies all requests starting with `/{randomBasePath}/` to the Kibana server +If you're writing scripts that interact with the Kibana API, the base path proxy will likely +make this difficult. To bypass the base path proxy for a single request, prefix urls with +`__UNSAFE_bypassBasePath` and the request will be routed to the development Kibana server. + +["source","shell"] +----------- +curl "http://elastic:changeme@localhost:5601/__UNSAFE_bypassBasePath/api/status" +----------- + This proxy can sometimes have unintended side effects in development, so when needed you can opt out by passing the `--no-base-path` flag to the `serve` task or `yarn start`. diff --git a/docs/development/core/development-functional-tests.asciidoc b/docs/development/core/development-functional-tests.asciidoc index ec65e2519e53..6d2c5a72f053 100644 --- a/docs/development/core/development-functional-tests.asciidoc +++ b/docs/development/core/development-functional-tests.asciidoc @@ -61,14 +61,29 @@ export TEST_ES_PASS= node scripts/functional_test_runner ---------- -** Selenium tests can be run in headless mode by setting the environment variable below. Unset this variable to show the browser. +** If you are running x-pack functional tests, start server and runner from {blob}xpack[x-pack] folder: ++ +["source", "shell"] +---------- +node scripts/functional_tests_server.js +node ../scripts/functional_test_runner.js +---------- + +** Selenium tests are run in headless mode on CI. Locally the same tests will be executed in a real browser. You can activate headless mode by setting the environment variable below: + ["source", "shell"] ---------- export TEST_BROWSER_HEADLESS=1 ---------- -** When running against cloud deployment, some tests are not applicable use --exclude-tag to skip those tests. An example shell file can be found at: {blob}test/scripts/jenkins_kibana.sh[test/scripts/jenkins_kibana.sh] +** If you are using Google Chrome, you can slow down the local network connection to verify test stability: ++ +["source", "shell"] +---------- +export TEST_THROTTLE_NETWORK=1 +---------- + +** When running against a Cloud deployment, some tests are not applicable. To skip tests that do not apply, use --exclude-tag. An example shell file can be found at: {blob}test/scripts/jenkins_cloud.sh[test/scripts/jenkins_cloud.sh] + ["source", "shell"] ---------- @@ -80,7 +95,8 @@ node scripts/functional_test_runner --exclude-tag skipCloud When run without any arguments the `FunctionalTestRunner` automatically loads the configuration in the standard location, but you can override that behavior with the `--config` flag. List configs with multiple --config arguments. -* `--config test/functional/config.js` starts Elasticsearch and Kibana servers with the selenium tests configuration. +* `--config test/functional/config.js` starts Elasticsearch and Kibana servers with the WebDriver tests configured to run in Chrome. +* `--config test/functional/config.firefox.js` starts Elasticsearch and Kibana servers with the WebDriver tests configured to run in Firefox. * `--config test/api_integration/config.js` starts Elasticsearch and Kibana servers with the api integration tests configuration. There are also command line flags for `--bail` and `--grep`, which behave just like their mocha counterparts. For instance, use `--grep=foo` to run only tests that match a regular expression. @@ -98,7 +114,7 @@ Use the `--help` flag for more options. The tests are written in https://mochajs.org[mocha] using https://github.com/elastic/kibana/tree/master/packages/kbn-expect[@kbn/expect] for assertions. -We use https://sites.google.com/a/chromium.org/chromedriver/[chromedriver], https://theintern.github.io/leadfoot[leadfoot], and https://github.com/theintern/digdug[digdug] for automating Chrome. When the `FunctionalTestRunner` launches, digdug opens a `Tunnel` which starts chromedriver and a stripped-down instance of Chrome. It also creates an instance of https://theintern.github.io/leadfoot/module-leadfoot_Command.html[Leadfoot's `Command`] class, which is available via the `remote` service. The `remote` communicates to Chrome through the digdug `Tunnel`. See the https://theintern.github.io/leadfoot/module-leadfoot_Command.html[leadfoot/Command API] docs for all the commands you can use with `remote`. +We use https://www.w3.org/TR/webdriver1/[WebDriver Protocol] to run tests in both Chrome and Firefox with the help of https://sites.google.com/a/chromium.org/chromedriver/[chromedriver] and https://firefox-source-docs.mozilla.org/testing/geckodriver/[geckodriver]. When the `FunctionalTestRunner` launches, remote service creates a new webdriver session, which starts the driver and a stripped-down browser instance. We use `browser` service and `webElementWrapper` class to wrap up https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/[Webdriver API]. The `FunctionalTestRunner` automatically transpiles functional tests using babel, so that tests can use the same ECMAScript features that Kibana source code uses. See {blob}style_guides/js_style_guide.md[style_guides/js_style_guide.md]. @@ -133,6 +149,34 @@ The `FunctionalTestRunner`'s primary purpose is to execute test files. These fil **Test Suite**::: A test suite is a collection of tests defined by calling `describe()`, and then populated with tests and setup/teardown hooks by calling `it()`, `before()`, `beforeEach()`, etc. Every test file must define only one top level test suite, and test suites can have as many nested test suites as they like. +**Tags**::: +Use tags in `describe()` function to group functional tests. Tags include: +* `ciGroup{id}` - Assigns test suite to a specific CI worker +* `skipCloud` and `skipFirefox` - Excludes test suite from running on Cloud or Firefox +* `smoke` - Groups tests that run on Chrome and Firefox + +**Cross-browser testing**::: +On CI, all the functional tests are executed in Chrome by default. To also run a suite against Firefox, assign the `smoke` tag: + +["source","js"] +----------- +// on CI test suite will be run twice: in Chrome and Firefox +describe('My Cross-browser Test Suite', function () { + this.tags('smoke'); + + it('My First Test'); +} +----------- + +If the tests do not apply to Firefox, assign the `skipFirefox` tag. + +To run tests on Firefox locally, use `config.firefox.js`: + +["source","shell"] +----------- +node scripts/functional_test_runner --config test/functional/config.firefox.js +----------- + [float] ===== Anatomy of a test file @@ -201,7 +245,7 @@ The first and only argument to all providers is a Provider API Object. This obje Within config files the API has the following properties [horizontal] -`log`::: An instance of the {blob}src/utils/tooling_log/tooling_log.js[`ToolingLog`] that is ready for use +`log`::: An instance of the {blob}packages/kbn-dev-utils/src/tooling_log/tooling_log.js[`ToolingLog`] that is ready for use `readConfigFile(path)`::: Returns a promise that will resolve to a Config instance that provides the values from the config file at `path` Within service and PageObject Providers the API is: @@ -224,17 +268,17 @@ Within a test Provider the API is exactly the same as the service providers API The `FunctionalTestRunner` comes with three built-in services: **config:**::: -* Source: {blob}src/functional_test_runner/lib/config/config.js[src/functional_test_runner/lib/config/config.js] -* Schema: {blob}src/functional_test_runner/lib/config/schema.js[src/functional_test_runner/lib/config/schema.js] +* Source: {blob}src/functional_test_runner/lib/config/config.ts[src/functional_test_runner/lib/config/config.ts] +* Schema: {blob}src/functional_test_runner/lib/config/schema.ts[src/functional_test_runner/lib/config/schema.ts] * Use `config.get(path)` to read any value from the config file **log:**::: -* Source: {blob}src/utils/tooling_log/tooling_log.js[src/utils/tooling_log/tooling_log.js] +* Source: {blob}packages/kbn-dev-utils/src/tooling_log/tooling_log.js[packages/kbn-dev-utils/src/tooling_log/tooling_log.js] * `ToolingLog` instances are readable streams. The instance provided by this service is automatically piped to stdout by the `FunctionalTestRunner` CLI * `log.verbose()`, `log.debug()`, `log.info()`, `log.warning()` all work just like console.log but produce more organized output **lifecycle:**::: -* Source: {blob}src/functional_test_runner/lib/lifecycle.js[src/functional_test_runner/lib/lifecycle.js] +* Source: {blob}src/functional_test_runner/lib/lifecycle.ts[src/functional_test_runner/lib/lifecycle.ts] * Designed primary for use in services * Exposes lifecycle events for basic coordination. Handlers can return a promise and resolve/fail asynchronously * Phases include: `beforeLoadTests`, `beforeTests`, `beforeEachTest`, `cleanup`, `phaseStart`, `phaseEnd` @@ -244,15 +288,15 @@ The `FunctionalTestRunner` comes with three built-in services: The Kibana functional tests define the vast majority of the actual functionality used by tests. -**retry:**::: -* Source: {blob}test/functional/services/retry.js[test/functional/services/retry.js] -* Helpers for retrying operations +**browser**::: +* Source: {blob}test/functional/services/browser.ts[test/functional/services/browser.ts] +* Higher level wrapper for `remote` service, which exposes available browser actions * Popular methods: -** `retry.try(fn)` - execute `fn` in a loop until it succeeds or the default try timeout elapses -** `retry.tryForTime(ms, fn)` execute fn in a loop until it succeeds or `ms` milliseconds elapses +** `browser.getWindowSize()` +** `browser.refresh()` **testSubjects:**::: -* Source: {blob}test/functional/services/test_subjects.js[test/functional/services/test_subjects.js] +* Source: {blob}test/functional/services/test_subjects.ts[test/functional/services/test_subjects.ts] * Test subjects are elements that are tagged specifically for selecting from tests * Use `testSubjects` over CSS selectors when possible * Usage: @@ -277,14 +321,21 @@ await testSubjects.click(‘containerButton’); ** `testSubjects.click(testSubjectSelector)` - Click a test subject in the page; throw if it can't be found after some time **find:**::: -* Source: {blob}test/functional/services/find.js[test/functional/services/find.js] +* Source: {blob}test/functional/services/find.ts[test/functional/services/find.ts] * Helpers for `remote.findBy*` methods that log and manage timeouts * Popular methods: ** `find.byCssSelector()` ** `find.allByCssSelector()` +**retry:**::: +* Source: {blob}test/common/services/retry/retry.ts[test/common/services/retry/retry.ts] +* Helpers for retrying operations +* Popular methods: +** `retry.try(fn, onFailureBlock)` - Execute `fn` in a loop until it succeeds or the default timeout elapses. The optional `onFailureBlock` is executed before each retry attempt. +** `retry.tryForTime(ms, fn, onFailureBlock)` - Execute `fn` in a loop until it succeeds or `ms` milliseconds elapses. The optional `onFailureBlock` is executed before each retry attempt. + **kibanaServer:**::: -* Source: {blob}test/functional/services/kibana_server/kibana_server.js[test/functional/services/kibana_server/kibana_server.js] +* Source: {blob}test/common/services/kibana_server/kibana_server.js[test/common/services/kibana_server/kibana_server.js] * Helpers for interacting with Kibana's server * Commonly used methods: ** `kibanaServer.uiSettings.update()` @@ -292,32 +343,28 @@ await testSubjects.click(‘containerButton’); ** `kibanaServer.status.getOverallState()` **esArchiver:**::: -* Source: {blob}test/functional/services/es_archiver.js[test/functional/services/es_archiver.js] +* Source: {blob}test/common/services/es_archiver.ts[test/common/services/es_archiver.ts] * Load/unload archives created with the `esArchiver` * Popular methods: ** `esArchiver.load(name)` ** `esArchiver.loadIfNeeded(name)` ** `esArchiver.unload(name)` -**docTable:**::: -* Source: {blob}test/functional/services/doc_table.js[test/functional/services/doc_table.js] -* Helpers for interacting with doc tables +Full list of services that are used in functional tests can be found here: {blob}test/functional/services[test/functional/services] -**pointSeriesVis:**::: -* Source: {blob}test/functional/services/point_series_vis.js[test/functional/services/point_series_vis.js] -* Helpers for interacting with point series visualizations **Low-level utilities:**::: * es -** Source: {blob}test/functional/services/es.js[test/functional/services/es.js] +** Source: {blob}test/common/services/es.ts[test/common/services/es.ts] ** Elasticsearch client ** Higher level options: `kibanaServer.uiSettings` or `esArchiver` * remote -** Source: {blob}test/functional/services/remote/remote.js[test/functional/services/remote/remote.js] -** Instance of https://theintern.github.io/leadfoot/module-leadfoot_Command.html[Leadfoot's `Command]` class +** Source: {blob}test/functional/services/remote/remote.ts[test/functional/services/remote/remote.ts] +** Instance of https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html[WebDriver] class ** Responsible for all communication with the browser -** Higher level options: `testSubjects`, `find`, and `PageObjects.common` -** See the https://theintern.github.io/leadfoot/module-leadfoot_Command.html[leadfoot/Command API] for full API +** To perform browser actions, use `remote` service +** For searching and manipulating with DOM elements, use `testSubjects` and `find` services +** See the https://seleniumhq.github.io/selenium/docs/api/javascript/[selenium-webdriver docs] for the full API. [float] ===== Custom Services diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md b/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md index bca2a7046d7c..8bbd1dfcd31f 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md @@ -4,8 +4,10 @@ ## ApplicationStart.availableApps property +Apps available based on the current capabilities. Should be used to show navigation links and make routing decisions. + Signature: ```typescript -availableApps: CapabilitiesStart['availableApps']; +availableApps: readonly App[]; ``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.capabilities.md b/docs/development/core/public/kibana-plugin-public.applicationstart.capabilities.md index 9ef82592f875..14326197ea54 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.capabilities.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.capabilities.md @@ -4,8 +4,10 @@ ## ApplicationStart.capabilities property +Gets the read-only capabilities. + Signature: ```typescript -capabilities: CapabilitiesStart['capabilities']; +capabilities: RecursiveReadonly; ``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 820d75cbd0e1..5854a7c65714 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -4,6 +4,7 @@ ## ApplicationStart interface + Signature: ```typescript @@ -14,7 +15,6 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | -| [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) | CapabilitiesStart['availableApps'] | | -| [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | CapabilitiesStart['capabilities'] | | -| [mount](./kibana-plugin-public.applicationstart.mount.md) | (mountHandler: Function) => void | | +| [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) | readonly App[] | Apps available based on the current capabilities. Should be used to show navigation links and make routing decisions. | +| [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.mount.md b/docs/development/core/public/kibana-plugin-public.applicationstart.mount.md deleted file mode 100644 index c6fd7872348b..000000000000 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.mount.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [mount](./kibana-plugin-public.applicationstart.mount.md) - -## ApplicationStart.mount property - -Signature: - -```typescript -mount: (mountHandler: Function) => void; -``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md index e1546488ba42..9cb278916dc4 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.active.md @@ -4,9 +4,11 @@ ## ChromeNavLink.active property -Indicates whether or not this app is currently on the screen. +> Warning: This API is now obsolete. +> +> -NOTE: remove this when ApplicationService is implemented and managing apps. +Indicates whether or not this app is currently on the screen. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md index 8dacb95e3aa1..d2b30530dd55 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.disabled.md @@ -4,9 +4,11 @@ ## ChromeNavLink.disabled property -Disables a link from being clickable. +> Warning: This API is now obsolete. +> +> -NOTE: 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. +Disables a link from being clickable. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md index 431ef0e6b877..6d04ab6d7885 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.hidden.md @@ -6,8 +6,6 @@ Hides a link from the navigation. -NOTE: remove this when ApplicationService is implemented. Instead, plugins should only register an Application if needed. - Signature: ```typescript diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md index fa7020ae52bb..7d76f4dc62be 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.linktolastsuburl.md @@ -4,9 +4,11 @@ ## ChromeNavLink.linkToLastSubUrl property -Whether or not the subUrl feature should be enabled. +> Warning: This API is now obsolete. +> +> -NOTE: only read by legacy platform. +Whether or not the subUrl feature should be enabled. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md index b7696aec74be..93ebbe3653ac 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -15,17 +15,17 @@ export interface ChromeNavLink | Property | Type | Description | | --- | --- | --- | -| [active](./kibana-plugin-public.chromenavlink.active.md) | boolean | Indicates whether or not this app is currently on the screen.NOTE: remove this when ApplicationService is implemented and managing apps. | +| [active](./kibana-plugin-public.chromenavlink.active.md) | boolean | Indicates whether or not this app is currently on the screen. | | [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) | string | The base route used to open the root of an application. | -| [disabled](./kibana-plugin-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable.NOTE: 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. | +| [disabled](./kibana-plugin-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | | [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | -| [hidden](./kibana-plugin-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation.NOTE: remove this when ApplicationService is implemented. Instead, plugins should only register an Application if needed. | +| [hidden](./kibana-plugin-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | | [icon](./kibana-plugin-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-public.chromenavlink.id.md) | string | A unique identifier for looking up links. | -| [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled.NOTE: only read by legacy platform. | +| [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled. | | [order](./kibana-plugin-public.chromenavlink.order.md) | number | An ordinal used to sort nav links relative to one another for display. | -| [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an applcation.NOTE: this should be removed once legacy apps are gone. | +| [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an applcation. | | [title](./kibana-plugin-public.chromenavlink.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | -| [url](./kibana-plugin-public.chromenavlink.url.md) | string | A url that legacy apps can set to deep link into their applications.NOTE: Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should be removed once the ApplicationService is implemented and mounting apps. At that time, each app can handle opening to the previous location when they are mounted. | +| [url](./kibana-plugin-public.chromenavlink.url.md) | string | A url that legacy apps can set to deep link into their applications. | diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md index b3957f22611f..b9d12432a01d 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.suburlbase.md @@ -4,9 +4,11 @@ ## ChromeNavLink.subUrlBase property -A url base that legacy apps can set to match deep URLs to an applcation. +> Warning: This API is now obsolete. +> +> -NOTE: this should be removed once legacy apps are gone. +A url base that legacy apps can set to match deep URLs to an applcation. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md index ce9f502fd5d3..33bd8fa3411d 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.url.md @@ -4,9 +4,11 @@ ## ChromeNavLink.url property -A url that legacy apps can set to deep link into their applications. +> Warning: This API is now obsolete. +> +> -NOTE: Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should be removed once the ApplicationService is implemented and mounting apps. At that time, each app can handle opening to the previous location when they are mounted. +A url that legacy apps can set to deep link into their applications. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md b/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md new file mode 100644 index 000000000000..2644596354e3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextSetup](./kibana-plugin-public.contextsetup.md) > [createContextContainer](./kibana-plugin-public.contextsetup.createcontextcontainer.md) + +## ContextSetup.createContextContainer() method + +Creates a new [IContextContainer](./kibana-plugin-public.icontextcontainer.md) for a service owner. + +Signature: + +```typescript +createContextContainer(): IContextContainer; +``` +Returns: + +`IContextContainer` + diff --git a/docs/development/core/public/kibana-plugin-public.contextsetup.md b/docs/development/core/public/kibana-plugin-public.contextsetup.md new file mode 100644 index 000000000000..d9c158fcaae0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.contextsetup.md @@ -0,0 +1,139 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ContextSetup](./kibana-plugin-public.contextsetup.md) + +## ContextSetup interface + +An object that handles registration of context providers and configuring handlers with context. + +Signature: + +```typescript +export interface ContextSetup +``` + +## Methods + +| Method | Description | +| --- | --- | +| [createContextContainer()](./kibana-plugin-public.contextsetup.createcontextcontainer.md) | Creates a new [IContextContainer](./kibana-plugin-public.icontextcontainer.md) for a service owner. | + +## Remarks + +A [IContextContainer](./kibana-plugin-public.icontextcontainer.md) can be used by any Core service or plugin (known as the "service owner") which wishes to expose APIs in a handler function. The container object will manage registering context providers and configuring a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the dependencies that the handler's plugin declares. + +Contexts providers are executed in the order they were registered. Each provider gets access to context values provided by any plugins that it depends on. + +In order to configure a handler with context, you must call the [IContextContainer.createHandler()](./kibana-plugin-public.icontextcontainer.createhandler.md) function and use the returned handler which will automatically build a context object when called. + +When registering context or creating handlers, the \_calling plugin's opaque id\_ must be provided. This id is passed in via the plugin's initializer and can be accessed from the [PluginInitializerContext.opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) Note this should NOT be the context service owner's id, but the plugin that is actually registering the context or handler. + +```ts +// Correct +class MyPlugin { + private readonly handlers = new Map(); + + setup(core) { + this.contextContainer = core.context.createContextContainer(); + return { + registerContext(pluginOpaqueId, contextName, provider) { + this.contextContainer.registerContext(pluginOpaqueId, contextName, provider); + }, + registerRoute(pluginOpaqueId, path, handler) { + this.handlers.set( + path, + this.contextContainer.createHandler(pluginOpaqueId, handler) + ); + } + } + } +} + +// Incorrect +class MyPlugin { + private readonly handlers = new Map(); + + constructor(private readonly initContext: PluginInitializerContext) {} + + setup(core) { + this.contextContainer = core.context.createContextContainer(); + return { + registerContext(contextName, provider) { + // BUG! + // This would leak this context to all handlers rather that only plugins that depend on the calling plugin. + this.contextContainer.registerContext(this.initContext.opaqueId, contextName, provider); + }, + registerRoute(path, handler) { + this.handlers.set( + path, + // BUG! + // This handler will not receive any contexts provided by other dependencies of the calling plugin. + this.contextContainer.createHandler(this.initContext.opaqueId, handler) + ); + } + } + } +} + +``` + +## Example + +Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. + +```ts +export interface VizRenderContext { + core: { + i18n: I18nStart; + uiSettings: UISettingsClientContract; + } + [contextName: string]: unknown; +} + +export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; + +class VizRenderingPlugin { + private readonly vizRenderers = new Map () => void)>(); + + constructor(private readonly initContext: PluginInitializerContext) {} + + setup(core) { + this.contextContainer = core.context.createContextContainer< + VizRenderContext, + ReturnType, + [HTMLElement] + >(); + + return { + registerContext: this.contextContainer.registerContext, + registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) => + this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)), + }; + } + + start(core) { + // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. + this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ + i18n: core.i18n, + uiSettings: core.uiSettings + })); + + return { + registerContext: this.contextContainer.registerContext, + + renderVizualization: (renderMethod: string, domElement: HTMLElement) => { + if (!this.vizRenderer.has(renderMethod)) { + throw new Error(`Render method '${renderMethod}' has not been registered`); + } + + // The handler can now be called directly with only an `HTMLElement` and will automatically + // have a new `context` object created and populated by the context container. + const handler = this.vizRenderers.get(renderMethod) + return handler(domElement); + } + }; + } +} + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.context.md b/docs/development/core/public/kibana-plugin-public.coresetup.context.md new file mode 100644 index 000000000000..e56ecb92074c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.coresetup.context.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [context](./kibana-plugin-public.coresetup.context.md) + +## CoreSetup.context property + +[ContextSetup](./kibana-plugin-public.contextsetup.md) + +Signature: + +```typescript +context: ContextSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index 5bbd54a2561a..a4b5b88df36d 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -16,6 +16,7 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | +| [context](./kibana-plugin-public.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-public.contextsetup.md) | | [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | FatalErrorsSetup | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | | [http](./kibana-plugin-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-public.httpsetup.md) | | [notifications](./kibana-plugin-public.coresetup.notifications.md) | NotificationsSetup | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index d22bcb09a105..446e45873521 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -23,5 +23,6 @@ export interface CoreStart | [i18n](./kibana-plugin-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-public.i18nstart.md) | | [notifications](./kibana-plugin-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | [overlays](./kibana-plugin-public.corestart.overlays.md) | OverlayStart | [OverlayStart](./kibana-plugin-public.overlaystart.md) | +| [savedObjects](./kibana-plugin-public.corestart.savedobjects.md) | SavedObjectsStart | [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | | [uiSettings](./kibana-plugin-public.corestart.uisettings.md) | UiSettingsClientContract | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-public.corestart.savedobjects.md b/docs/development/core/public/kibana-plugin-public.corestart.savedobjects.md new file mode 100644 index 000000000000..5e6e0e33c7f8 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.corestart.savedobjects.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [savedObjects](./kibana-plugin-public.corestart.savedobjects.md) + +## CoreStart.savedObjects property + +[SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) + +Signature: + +```typescript +savedObjects: SavedObjectsStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpbody.md b/docs/development/core/public/kibana-plugin-public.httpbody.md new file mode 100644 index 000000000000..ab31f28b8dc3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpbody.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpBody](./kibana-plugin-public.httpbody.md) + +## HttpBody type + + +Signature: + +```typescript +export declare type HttpBody = BodyInit | null | any; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httperrorrequest.error.md b/docs/development/core/public/kibana-plugin-public.httperrorrequest.error.md new file mode 100644 index 000000000000..a8b511f889cd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httperrorrequest.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) > [error](./kibana-plugin-public.httperrorrequest.error.md) + +## HttpErrorRequest.error property + +Signature: + +```typescript +error: Error; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httperrorrequest.md b/docs/development/core/public/kibana-plugin-public.httperrorrequest.md new file mode 100644 index 000000000000..e28d092eda71 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httperrorrequest.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) + +## HttpErrorRequest interface + + +Signature: + +```typescript +export interface HttpErrorRequest +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-public.httperrorrequest.error.md) | Error | | +| [request](./kibana-plugin-public.httperrorrequest.request.md) | Request | | + diff --git a/docs/development/core/public/kibana-plugin-public.httperrorrequest.request.md b/docs/development/core/public/kibana-plugin-public.httperrorrequest.request.md new file mode 100644 index 000000000000..7a8a33307612 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httperrorrequest.request.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) > [request](./kibana-plugin-public.httperrorrequest.request.md) + +## HttpErrorRequest.request property + +Signature: + +```typescript +request?: Request; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httperrorresponse.error.md b/docs/development/core/public/kibana-plugin-public.httperrorresponse.error.md new file mode 100644 index 000000000000..cb82a1f37f84 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httperrorresponse.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) > [error](./kibana-plugin-public.httperrorresponse.error.md) + +## HttpErrorResponse.error property + +Signature: + +```typescript +error: Error | HttpFetchError; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httperrorresponse.md b/docs/development/core/public/kibana-plugin-public.httperrorresponse.md new file mode 100644 index 000000000000..ff001e4401c6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httperrorresponse.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) + +## HttpErrorResponse interface + + +Signature: + +```typescript +export interface HttpErrorResponse extends HttpResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-public.httperrorresponse.error.md) | Error | HttpFetchError | | + diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.headers.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.headers.md new file mode 100644 index 000000000000..2fb4c448fe23 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.headers.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) > [headers](./kibana-plugin-public.httpfetchoptions.headers.md) + +## HttpFetchOptions.headers property + +Signature: + +```typescript +headers?: HttpHeadersInit; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md new file mode 100644 index 000000000000..93fabb053871 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) + +## HttpFetchOptions interface + + +Signature: + +```typescript +export interface HttpFetchOptions extends HttpRequestInit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-public.httpfetchoptions.headers.md) | HttpHeadersInit | | +| [prependBasePath](./kibana-plugin-public.httpfetchoptions.prependbasepath.md) | boolean | | +| [query](./kibana-plugin-public.httpfetchoptions.query.md) | HttpFetchQuery | | + diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.prependbasepath.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.prependbasepath.md new file mode 100644 index 000000000000..5fff6c3518b5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.prependbasepath.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) > [prependBasePath](./kibana-plugin-public.httpfetchoptions.prependbasepath.md) + +## HttpFetchOptions.prependBasePath property + +Signature: + +```typescript +prependBasePath?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.query.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.query.md new file mode 100644 index 000000000000..2c24a3a3a548 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.query.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) > [query](./kibana-plugin-public.httpfetchoptions.query.md) + +## HttpFetchOptions.query property + +Signature: + +```typescript +query?: HttpFetchQuery; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchquery.md b/docs/development/core/public/kibana-plugin-public.httpfetchquery.md new file mode 100644 index 000000000000..e09b22b07445 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchquery.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) + +## HttpFetchQuery interface + + +Signature: + +```typescript +export interface HttpFetchQuery +``` diff --git a/docs/development/core/public/kibana-plugin-public.httphandler.md b/docs/development/core/public/kibana-plugin-public.httphandler.md new file mode 100644 index 000000000000..8bc9c3302252 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httphandler.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpHandler](./kibana-plugin-public.httphandler.md) + +## HttpHandler type + + +Signature: + +```typescript +export declare type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpheadersinit.md b/docs/development/core/public/kibana-plugin-public.httpheadersinit.md new file mode 100644 index 000000000000..15877a55fcdd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpheadersinit.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) + +## HttpHeadersInit interface + + +Signature: + +```typescript +export interface HttpHeadersInit +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.body.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.body.md new file mode 100644 index 000000000000..ecf8343ab529 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.body.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [body](./kibana-plugin-public.httprequestinit.body.md) + +## HttpRequestInit.body property + +Signature: + +```typescript +body?: BodyInit | null; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.cache.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.cache.md new file mode 100644 index 000000000000..813639b51f81 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.cache.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [cache](./kibana-plugin-public.httprequestinit.cache.md) + +## HttpRequestInit.cache property + +Signature: + +```typescript +cache?: RequestCache; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.credentials.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.credentials.md new file mode 100644 index 000000000000..26e86722a821 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.credentials.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [credentials](./kibana-plugin-public.httprequestinit.credentials.md) + +## HttpRequestInit.credentials property + +Signature: + +```typescript +credentials?: RequestCredentials; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.headers.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.headers.md new file mode 100644 index 000000000000..2e5f86ebe38e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.headers.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [headers](./kibana-plugin-public.httprequestinit.headers.md) + +## HttpRequestInit.headers property + +Signature: + +```typescript +headers?: HttpHeadersInit; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.integrity.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.integrity.md new file mode 100644 index 000000000000..9d8b3644aa9d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.integrity.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [integrity](./kibana-plugin-public.httprequestinit.integrity.md) + +## HttpRequestInit.integrity property + +Signature: + +```typescript +integrity?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.keepalive.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.keepalive.md new file mode 100644 index 000000000000..bb1a50c280dc --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.keepalive.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [keepalive](./kibana-plugin-public.httprequestinit.keepalive.md) + +## HttpRequestInit.keepalive property + +Signature: + +```typescript +keepalive?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.md new file mode 100644 index 000000000000..89fa6d537958 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.md @@ -0,0 +1,31 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) + +## HttpRequestInit interface + + +Signature: + +```typescript +export interface HttpRequestInit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-public.httprequestinit.body.md) | BodyInit | null | | +| [cache](./kibana-plugin-public.httprequestinit.cache.md) | RequestCache | | +| [credentials](./kibana-plugin-public.httprequestinit.credentials.md) | RequestCredentials | | +| [headers](./kibana-plugin-public.httprequestinit.headers.md) | HttpHeadersInit | | +| [integrity](./kibana-plugin-public.httprequestinit.integrity.md) | string | | +| [keepalive](./kibana-plugin-public.httprequestinit.keepalive.md) | boolean | | +| [method](./kibana-plugin-public.httprequestinit.method.md) | string | | +| [mode](./kibana-plugin-public.httprequestinit.mode.md) | RequestMode | | +| [redirect](./kibana-plugin-public.httprequestinit.redirect.md) | RequestRedirect | | +| [referrer](./kibana-plugin-public.httprequestinit.referrer.md) | string | | +| [referrerPolicy](./kibana-plugin-public.httprequestinit.referrerpolicy.md) | ReferrerPolicy | | +| [signal](./kibana-plugin-public.httprequestinit.signal.md) | AbortSignal | null | | +| [window](./kibana-plugin-public.httprequestinit.window.md) | any | | + diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.method.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.method.md new file mode 100644 index 000000000000..2aab89940557 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.method.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [method](./kibana-plugin-public.httprequestinit.method.md) + +## HttpRequestInit.method property + +Signature: + +```typescript +method?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.mode.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.mode.md new file mode 100644 index 000000000000..611671331ee5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.mode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [mode](./kibana-plugin-public.httprequestinit.mode.md) + +## HttpRequestInit.mode property + +Signature: + +```typescript +mode?: RequestMode; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.redirect.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.redirect.md new file mode 100644 index 000000000000..6795e99d370f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.redirect.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [redirect](./kibana-plugin-public.httprequestinit.redirect.md) + +## HttpRequestInit.redirect property + +Signature: + +```typescript +redirect?: RequestRedirect; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.referrer.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.referrer.md new file mode 100644 index 000000000000..60e249cc9cf1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.referrer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [referrer](./kibana-plugin-public.httprequestinit.referrer.md) + +## HttpRequestInit.referrer property + +Signature: + +```typescript +referrer?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.referrerpolicy.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.referrerpolicy.md new file mode 100644 index 000000000000..3f92ee021f9c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.referrerpolicy.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [referrerPolicy](./kibana-plugin-public.httprequestinit.referrerpolicy.md) + +## HttpRequestInit.referrerPolicy property + +Signature: + +```typescript +referrerPolicy?: ReferrerPolicy; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.signal.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.signal.md new file mode 100644 index 000000000000..8657c6b7a124 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.signal.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [signal](./kibana-plugin-public.httprequestinit.signal.md) + +## HttpRequestInit.signal property + +Signature: + +```typescript +signal?: AbortSignal | null; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httprequestinit.window.md b/docs/development/core/public/kibana-plugin-public.httprequestinit.window.md new file mode 100644 index 000000000000..aec7fad7e392 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httprequestinit.window.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) > [window](./kibana-plugin-public.httprequestinit.window.md) + +## HttpRequestInit.window property + +Signature: + +```typescript +window?: any; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.body.md b/docs/development/core/public/kibana-plugin-public.httpresponse.body.md new file mode 100644 index 000000000000..c590c9ec49d1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpresponse.body.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) > [body](./kibana-plugin-public.httpresponse.body.md) + +## HttpResponse.body property + +Signature: + +```typescript +body?: HttpBody; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.md b/docs/development/core/public/kibana-plugin-public.httpresponse.md new file mode 100644 index 000000000000..b2ec48fd4d6b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpresponse.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) + +## HttpResponse interface + + +Signature: + +```typescript +export interface HttpResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-public.httpresponse.body.md) | HttpBody | | +| [request](./kibana-plugin-public.httpresponse.request.md) | Request | | +| [response](./kibana-plugin-public.httpresponse.response.md) | Response | | + diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.request.md b/docs/development/core/public/kibana-plugin-public.httpresponse.request.md new file mode 100644 index 000000000000..3aaae6f8af09 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpresponse.request.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) > [request](./kibana-plugin-public.httpresponse.request.md) + +## HttpResponse.request property + +Signature: + +```typescript +request: Request; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.response.md b/docs/development/core/public/kibana-plugin-public.httpresponse.response.md new file mode 100644 index 000000000000..44c8eb4295f1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpresponse.response.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) > [response](./kibana-plugin-public.httpresponse.response.md) + +## HttpResponse.response property + +Signature: + +```typescript +response?: Response; +``` diff --git a/docs/development/core/public/kibana-plugin-public.icontextcontainer.createhandler.md b/docs/development/core/public/kibana-plugin-public.icontextcontainer.createhandler.md new file mode 100644 index 000000000000..a02cc0f2e0a3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.icontextcontainer.createhandler.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextContainer](./kibana-plugin-public.icontextcontainer.md) > [createHandler](./kibana-plugin-public.icontextcontainer.createhandler.md) + +## IContextContainer.createHandler() method + +Create a new handler function pre-wired to context for the plugin. + +Signature: + +```typescript +createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler): (...rest: THandlerParameters) => Promisify; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| pluginOpaqueId | PluginOpaqueId | The plugin opaque ID for the plugin that registers this handler. | +| handler | IContextHandler<TContext, THandlerReturn, THandlerParameters> | Handler function to pass context object to. | + +Returns: + +`(...rest: THandlerParameters) => Promisify` + +A function that takes `THandlerParameters`, calls `handler` with a new context, and returns a Promise of the `handler` return value. + diff --git a/docs/development/core/public/kibana-plugin-public.icontextcontainer.md b/docs/development/core/public/kibana-plugin-public.icontextcontainer.md new file mode 100644 index 000000000000..0bc7c8f3808d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.icontextcontainer.md @@ -0,0 +1,80 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextContainer](./kibana-plugin-public.icontextcontainer.md) + +## IContextContainer interface + +An object that handles registration of context providers and configuring handlers with context. + +Signature: + +```typescript +export interface IContextContainer +``` + +## Methods + +| Method | Description | +| --- | --- | +| [createHandler(pluginOpaqueId, handler)](./kibana-plugin-public.icontextcontainer.createhandler.md) | Create a new handler function pre-wired to context for the plugin. | +| [registerContext(pluginOpaqueId, contextName, provider)](./kibana-plugin-public.icontextcontainer.registercontext.md) | Register a new context provider. | + +## Remarks + +A [IContextContainer](./kibana-plugin-public.icontextcontainer.md) can be used by any Core service or plugin (known as the "service owner") which wishes to expose APIs in a handler function. The container object will manage registering context providers and configuring a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the dependencies that the handler's plugin declares. + +Contexts providers are executed in the order they were registered. Each provider gets access to context values provided by any plugins that it depends on. + +In order to configure a handler with context, you must call the [IContextContainer.createHandler()](./kibana-plugin-public.icontextcontainer.createhandler.md) function and use the returned handler which will automatically build a context object when called. + +When registering context or creating handlers, the \_calling plugin's opaque id\_ must be provided. This id is passed in via the plugin's initializer and can be accessed from the [PluginInitializerContext.opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) Note this should NOT be the context service owner's id, but the plugin that is actually registering the context or handler. + +```ts +// Correct +class MyPlugin { + private readonly handlers = new Map(); + + setup(core) { + this.contextContainer = core.context.createContextContainer(); + return { + registerContext(pluginOpaqueId, contextName, provider) { + this.contextContainer.registerContext(pluginOpaqueId, contextName, provider); + }, + registerRoute(pluginOpaqueId, path, handler) { + this.handlers.set( + path, + this.contextContainer.createHandler(pluginOpaqueId, handler) + ); + } + } + } +} + +// Incorrect +class MyPlugin { + private readonly handlers = new Map(); + + constructor(private readonly initContext: PluginInitializerContext) {} + + setup(core) { + this.contextContainer = core.context.createContextContainer(); + return { + registerContext(contextName, provider) { + // BUG! + // This would leak this context to all handlers rather that only plugins that depend on the calling plugin. + this.contextContainer.registerContext(this.initContext.opaqueId, contextName, provider); + }, + registerRoute(path, handler) { + this.handlers.set( + path, + // BUG! + // This handler will not receive any contexts provided by other dependencies of the calling plugin. + this.contextContainer.createHandler(this.initContext.opaqueId, handler) + ); + } + } + } +} + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.icontextcontainer.registercontext.md b/docs/development/core/public/kibana-plugin-public.icontextcontainer.registercontext.md new file mode 100644 index 000000000000..2cf10a6ec841 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.icontextcontainer.registercontext.md @@ -0,0 +1,34 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextContainer](./kibana-plugin-public.icontextcontainer.md) > [registerContext](./kibana-plugin-public.icontextcontainer.registercontext.md) + +## IContextContainer.registerContext() method + +Register a new context provider. + +Signature: + +```typescript +registerContext(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| pluginOpaqueId | PluginOpaqueId | The plugin opaque ID for the plugin that registers this context. | +| contextName | TContextName | The key of the TContext object this provider supplies the value for. | +| provider | IContextProvider<TContext, TContextName, THandlerParameters> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) to be called each time a new context is created. | + +Returns: + +`this` + +The [IContextContainer](./kibana-plugin-public.icontextcontainer.md) for method chaining. + +## Remarks + +The value (or resolved Promise value) returned by the `provider` function will be attached to the context object on the key specified by `contextName`. + +Throws an exception if more than one provider is registered for the same `contextName`. + diff --git a/docs/development/core/public/kibana-plugin-public.icontexthandler.md b/docs/development/core/public/kibana-plugin-public.icontexthandler.md new file mode 100644 index 000000000000..2251b1131c31 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.icontexthandler.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextHandler](./kibana-plugin-public.icontexthandler.md) + +## IContextHandler type + +A function registered by a plugin to perform some action. + +Signature: + +```typescript +export declare type IContextHandler = (context: TContext, ...rest: THandlerParameters) => TReturn; +``` + +## Remarks + +A new `TContext` will be built for each handler before invoking. + diff --git a/docs/development/core/public/kibana-plugin-public.icontextprovider.md b/docs/development/core/public/kibana-plugin-public.icontextprovider.md new file mode 100644 index 000000000000..a84917d6e144 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.icontextprovider.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextProvider](./kibana-plugin-public.icontextprovider.md) + +## IContextProvider type + +A function that returns a context value for a specific key of given context type. + +Signature: + +```typescript +export declare type IContextProvider, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName]; +``` + +## Remarks + +This function will be called each time a new context is built for a handler invocation. + diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 98b6a8703f54..5fda9f915930 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -14,6 +14,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | Class | Description | | --- | --- | +| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. | +| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md).It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. | | [ToastsApi](./kibana-plugin-public.toastsapi.md) | | | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | | @@ -34,15 +36,24 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeRecentlyAccessed](./kibana-plugin-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-public.chromerecentlyaccessed.md) for recently accessed history. | | [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-public.chromerecentlyaccessedhistoryitem.md) | | | [ChromeStart](./kibana-plugin-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. | +| [ContextSetup](./kibana-plugin-public.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | | [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the Plugin setup lifecycle | | [CoreStart](./kibana-plugin-public.corestart.md) | Core services exposed to the Plugin start lifecycle | | [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | | | [ErrorToastOptions](./kibana-plugin-public.errortoastoptions.md) | | | [FatalErrorInfo](./kibana-plugin-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | +| [HttpErrorRequest](./kibana-plugin-public.httperrorrequest.md) | | +| [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | | +| [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | | +| [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | | +| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | | | [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | | +| [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | | +| [HttpResponse](./kibana-plugin-public.httpresponse.md) | | | [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | +| [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | | | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | @@ -50,6 +61,19 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OverlayStart](./kibana-plugin-public.overlaystart.md) | | | [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer | +| [SavedObject](./kibana-plugin-public.savedobject.md) | | +| [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) | The data for a Saved Object is stored in the attributes key as either an object or an array of objects. | +| [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) | A reference to another saved object. | +| [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) | | +| [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) | | +| [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) | | +| [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) | | +| [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | | +| [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | | +| [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | | +| [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) | | | [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | | ## Type Aliases @@ -58,10 +82,17 @@ The plugin integrates with the core system via lifecycle events: `setup` | --- | --- | | [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | +| [HttpBody](./kibana-plugin-public.httpbody.md) | | +| [HttpHandler](./kibana-plugin-public.httphandler.md) | | | [HttpSetup](./kibana-plugin-public.httpsetup.md) | | | [HttpStart](./kibana-plugin-public.httpstart.md) | | +| [IContextHandler](./kibana-plugin-public.icontexthandler.md) | A function registered by a plugin to perform some action. | +| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | +| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | | | [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | | +| [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | | +| [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | | [ToastInput](./kibana-plugin-public.toastinput.md) | | | [UiSettingsClientContract](./kibana-plugin-public.uisettingsclientcontract.md) | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.md b/docs/development/core/public/kibana-plugin-public.overlaystart.md index c14ba89a7ffd..1345beffbfb6 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaystart.md +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.md @@ -16,5 +16,5 @@ export interface OverlayStart | Property | Type | Description | | --- | --- | --- | | [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | (flyoutChildren: React.ReactNode, flyoutProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef | | -| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | (modalChildren: React.ReactNode, modalProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef | | +| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | (modalChildren: React.ReactNode, modalProps?: {
className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef | | diff --git a/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md b/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md index 0fc8ba164eae..a4569e178f17 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md +++ b/docs/development/core/public/kibana-plugin-public.overlaystart.openmodal.md @@ -8,6 +8,7 @@ ```typescript openModal: (modalChildren: React.ReactNode, modalProps?: { + className?: string; closeButtonAriaLabel?: string; 'data-test-subj'?: string; }) => OverlayRef; diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md index 5dbe464d1561..3ad220349c45 100644 --- a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.md @@ -11,3 +11,10 @@ The available core services passed to a `PluginInitializer` ```typescript export interface PluginInitializerContext ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) | PluginOpaqueId | A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. | + diff --git a/docs/development/core/public/kibana-plugin-public.plugininitializercontext.opaqueid.md b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.opaqueid.md new file mode 100644 index 000000000000..10e6b79be495 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.plugininitializercontext.opaqueid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) > [opaqueId](./kibana-plugin-public.plugininitializercontext.opaqueid.md) + +## PluginInitializerContext.opaqueId property + +A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. + +Signature: + +```typescript +readonly opaqueId: PluginOpaqueId; +``` diff --git a/docs/development/core/public/kibana-plugin-public.pluginopaqueid.md b/docs/development/core/public/kibana-plugin-public.pluginopaqueid.md new file mode 100644 index 000000000000..8a8202ae1334 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginopaqueid.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) + +## PluginOpaqueId type + + +Signature: + +```typescript +export declare type PluginOpaqueId = symbol; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.attributes.md b/docs/development/core/public/kibana-plugin-public.savedobject.attributes.md new file mode 100644 index 000000000000..f9d39c15fcff --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.attributes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [attributes](./kibana-plugin-public.savedobject.attributes.md) + +## SavedObject.attributes property + +The data for a Saved Object is stored in the `attributes` key as either an object or an array of objects. + +Signature: + +```typescript +attributes: T; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.error.md b/docs/development/core/public/kibana-plugin-public.savedobject.error.md new file mode 100644 index 000000000000..1d00863ef6ec --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.error.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [error](./kibana-plugin-public.savedobject.error.md) + +## SavedObject.error property + +Signature: + +```typescript +error?: { + message: string; + statusCode: number; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.id.md b/docs/development/core/public/kibana-plugin-public.savedobject.id.md new file mode 100644 index 000000000000..7b54e0a7c2a7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [id](./kibana-plugin-public.savedobject.id.md) + +## SavedObject.id property + +The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.md b/docs/development/core/public/kibana-plugin-public.savedobject.md new file mode 100644 index 000000000000..9bf0149f0854 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) + +## SavedObject interface + + +Signature: + +```typescript +export interface SavedObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [attributes](./kibana-plugin-public.savedobject.attributes.md) | T | The data for a Saved Object is stored in the attributes key as either an object or an array of objects. | +| [error](./kibana-plugin-public.savedobject.error.md) | {
message: string;
statusCode: number;
} | | +| [id](./kibana-plugin-public.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | +| [migrationVersion](./kibana-plugin-public.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [references](./kibana-plugin-public.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | +| [type](./kibana-plugin-public.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | +| [updated\_at](./kibana-plugin-public.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | +| [version](./kibana-plugin-public.savedobject.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.migrationversion.md b/docs/development/core/public/kibana-plugin-public.savedobject.migrationversion.md new file mode 100644 index 000000000000..d07b664f11ff --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.migrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [migrationVersion](./kibana-plugin-public.savedobject.migrationversion.md) + +## SavedObject.migrationVersion property + +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.references.md b/docs/development/core/public/kibana-plugin-public.savedobject.references.md new file mode 100644 index 000000000000..3c3ecdd283b5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.references.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [references](./kibana-plugin-public.savedobject.references.md) + +## SavedObject.references property + +A reference to another saved object. + +Signature: + +```typescript +references: SavedObjectReference[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.type.md b/docs/development/core/public/kibana-plugin-public.savedobject.type.md new file mode 100644 index 000000000000..2bce5b8a1563 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [type](./kibana-plugin-public.savedobject.type.md) + +## SavedObject.type property + +The type of Saved Object. Each plugin can define it's own custom Saved Object types. + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.updated_at.md b/docs/development/core/public/kibana-plugin-public.savedobject.updated_at.md new file mode 100644 index 000000000000..861128c69ae2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.updated_at.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [updated\_at](./kibana-plugin-public.savedobject.updated_at.md) + +## SavedObject.updated\_at property + +Timestamp of the last time this document had been updated. + +Signature: + +```typescript +updated_at?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.version.md b/docs/development/core/public/kibana-plugin-public.savedobject.version.md new file mode 100644 index 000000000000..26356f444f2c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.version.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [version](./kibana-plugin-public.savedobject.version.md) + +## SavedObject.version property + +An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectattribute.md b/docs/development/core/public/kibana-plugin-public.savedobjectattribute.md new file mode 100644 index 000000000000..f8d51390863e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectattribute.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) + +## SavedObjectAttribute type + + +Signature: + +```typescript +export declare type SavedObjectAttribute = string | number | boolean | null | undefined | SavedObjectAttributes | SavedObjectAttributes[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectattributes.md b/docs/development/core/public/kibana-plugin-public.savedobjectattributes.md new file mode 100644 index 000000000000..4a9e096cc25b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectattributes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) + +## SavedObjectAttributes interface + +The data for a Saved Object is stored in the `attributes` key as either an object or an array of objects. + +Signature: + +```typescript +export interface SavedObjectAttributes +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectreference.id.md b/docs/development/core/public/kibana-plugin-public.savedobjectreference.id.md new file mode 100644 index 000000000000..27b820607f86 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectreference.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) > [id](./kibana-plugin-public.savedobjectreference.id.md) + +## SavedObjectReference.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectreference.md b/docs/development/core/public/kibana-plugin-public.savedobjectreference.md new file mode 100644 index 000000000000..7ae05e24a5d8 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectreference.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) + +## SavedObjectReference interface + +A reference to another saved object. + +Signature: + +```typescript +export interface SavedObjectReference +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-public.savedobjectreference.id.md) | string | | +| [name](./kibana-plugin-public.savedobjectreference.name.md) | string | | +| [type](./kibana-plugin-public.savedobjectreference.type.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectreference.name.md b/docs/development/core/public/kibana-plugin-public.savedobjectreference.name.md new file mode 100644 index 000000000000..104a8c313b52 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectreference.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) > [name](./kibana-plugin-public.savedobjectreference.name.md) + +## SavedObjectReference.name property + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectreference.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectreference.type.md new file mode 100644 index 000000000000..5b55a18becab --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectreference.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) > [type](./kibana-plugin-public.savedobjectreference.type.md) + +## SavedObjectReference.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.md new file mode 100644 index 000000000000..00ea2fd15829 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) + +## SavedObjectsBaseOptions interface + + +Signature: + +```typescript +export interface SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [namespace](./kibana-plugin-public.savedobjectsbaseoptions.namespace.md) | string | Specify the namespace for this operation | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.namespace.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.namespace.md new file mode 100644 index 000000000000..fb8d0d957a27 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.namespace.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) > [namespace](./kibana-plugin-public.savedobjectsbaseoptions.namespace.md) + +## SavedObjectsBaseOptions.namespace property + +Specify the namespace for this operation + +Signature: + +```typescript +namespace?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md new file mode 100644 index 000000000000..2ccddb8f71bd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) + +## SavedObjectsBatchResponse interface + + +Signature: + +```typescript +export interface SavedObjectsBatchResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [savedObjects](./kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md) | Array<SimpleSavedObject<T>> | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md new file mode 100644 index 000000000000..f83b6268431c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) > [savedObjects](./kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md) + +## SavedObjectsBatchResponse.savedObjects property + +Signature: + +```typescript +savedObjects: Array>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md new file mode 100644 index 000000000000..e3f7e0d67608 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) > [attributes](./kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md) + +## SavedObjectsBulkCreateObject.attributes property + +Signature: + +```typescript +attributes: T; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md new file mode 100644 index 000000000000..8f95c0533dde --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) + +## SavedObjectsBulkCreateObject interface + +Signature: + +```typescript +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [attributes](./kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md) | T | | +| [type](./kibana-plugin-public.savedobjectsbulkcreateobject.type.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.type.md new file mode 100644 index 000000000000..37497b917878 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) > [type](./kibana-plugin-public.savedobjectsbulkcreateobject.type.md) + +## SavedObjectsBulkCreateObject.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.md new file mode 100644 index 000000000000..697084d8eee3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) + +## SavedObjectsBulkCreateOptions interface + + +Signature: + +```typescript +export interface SavedObjectsBulkCreateOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [overwrite](./kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md) | boolean | If a document with the given id already exists, overwrite it's contents (default=false). | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md new file mode 100644 index 000000000000..e3b425da892b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) > [overwrite](./kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md) + +## SavedObjectsBulkCreateOptions.overwrite property + +If a document with the given `id` already exists, overwrite it's contents (default=false). + +Signature: + +```typescript +overwrite?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md new file mode 100644 index 000000000000..426096d96c9b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [bulkCreate](./kibana-plugin-public.savedobjectsclient.bulkcreate.md) + +## SavedObjectsClient.bulkCreate property + +Creates multiple documents at once + +Signature: + +```typescript +bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md new file mode 100644 index 000000000000..fc8b3c8258f9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) + +## SavedObjectsClient.bulkGet property + +Returns an array of objects by id + +Signature: + +```typescript +bulkGet: (objects?: { + id: string; + type: string; + }[]) => Promise>; +``` + +## Example + +bulkGet(\[ { id: 'one', type: 'config' }, { id: 'foo', type: 'index-pattern' } \]) + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md new file mode 100644 index 000000000000..d6366494f003 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [create](./kibana-plugin-public.savedobjectsclient.create.md) + +## SavedObjectsClient.create property + +Persists an object + +Signature: + +```typescript +create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.delete.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.delete.md new file mode 100644 index 000000000000..03658cbd9fcf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.delete.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [delete](./kibana-plugin-public.savedobjectsclient.delete.md) + +## SavedObjectsClient.delete property + +Deletes an object + +Signature: + +```typescript +delete: (type: string, id: string) => Promise<{}>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md new file mode 100644 index 000000000000..20b9ff35779f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [find](./kibana-plugin-public.savedobjectsclient.find.md) + +## SavedObjectsClient.find property + +Search for objects + +Signature: + +```typescript +find: (options?: Pick) => Promise>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md new file mode 100644 index 000000000000..88500f4e3a26 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [get](./kibana-plugin-public.savedobjectsclient.get.md) + +## SavedObjectsClient.get property + +Fetches a single object + +Signature: + +```typescript +get: (type: string, id: string) => Promise>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md new file mode 100644 index 000000000000..d0de26bb9b0a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -0,0 +1,35 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) + +## SavedObjectsClient class + +Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. + +Signature: + +```typescript +export declare class SavedObjectsClient +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [bulkCreate](./kibana-plugin-public.savedobjectsclient.bulkcreate.md) | | (objects?: SavedObjectsBulkCreateObject<SavedObjectAttributes>[], options?: SavedObjectsBulkCreateOptions) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Creates multiple documents at once | +| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | +| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | +| [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options?: Pick<SavedObjectFindOptionsServer, "search" | "type" | "defaultSearchOperator" | "searchFields" | "sortField" | "hasReference" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [update(type, id, attributes, { version, migrationVersion, references })](./kibana-plugin-public.savedobjectsclient.update.md) | | Updates an object | + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `SavedObjectsClient` class. + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md new file mode 100644 index 000000000000..5f87f46d6206 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [update](./kibana-plugin-public.savedobjectsclient.update.md) + +## SavedObjectsClient.update() method + +Updates an object + +Signature: + +```typescript +update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| attributes | T | | +| { version, migrationVersion, references } | SavedObjectsUpdateOptions | | + +Returns: + +`Promise>` + + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclientcontract.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclientcontract.md new file mode 100644 index 000000000000..876f3164feec --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclientcontract.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) + +## SavedObjectsClientContract type + +SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) + +Signature: + +```typescript +export declare type SavedObjectsClientContract = PublicMethodsOf; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.id.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.id.md new file mode 100644 index 000000000000..fc0532a10d63 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) > [id](./kibana-plugin-public.savedobjectscreateoptions.id.md) + +## SavedObjectsCreateOptions.id property + +(Not recommended) Specify an id instead of having the saved objects service generate one for you. + +Signature: + +```typescript +id?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.md new file mode 100644 index 000000000000..08090c0f8d8c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) + +## SavedObjectsCreateOptions interface + + +Signature: + +```typescript +export interface SavedObjectsCreateOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-public.savedobjectscreateoptions.id.md) | string | (Not recommended) Specify an id instead of having the saved objects service generate one for you. | +| [migrationVersion](./kibana-plugin-public.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [overwrite](./kibana-plugin-public.savedobjectscreateoptions.overwrite.md) | boolean | If a document with the given id already exists, overwrite it's contents (default=false). | +| [references](./kibana-plugin-public.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.migrationversion.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.migrationversion.md new file mode 100644 index 000000000000..5bc6b62f6680 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.migrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) > [migrationVersion](./kibana-plugin-public.savedobjectscreateoptions.migrationversion.md) + +## SavedObjectsCreateOptions.migrationVersion property + +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.overwrite.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.overwrite.md new file mode 100644 index 000000000000..d83541fc9e87 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.overwrite.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) > [overwrite](./kibana-plugin-public.savedobjectscreateoptions.overwrite.md) + +## SavedObjectsCreateOptions.overwrite property + +If a document with the given `id` already exists, overwrite it's contents (default=false). + +Signature: + +```typescript +overwrite?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.references.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.references.md new file mode 100644 index 000000000000..f6bcd47a3e8d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) > [references](./kibana-plugin-public.savedobjectscreateoptions.references.md) + +## SavedObjectsCreateOptions.references property + +Signature: + +```typescript +references?: SavedObjectReference[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md new file mode 100644 index 000000000000..181e2bb237c5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [defaultSearchOperator](./kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md) + +## SavedObjectsFindOptions.defaultSearchOperator property + +Signature: + +```typescript +defaultSearchOperator?: 'AND' | 'OR'; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.fields.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.fields.md new file mode 100644 index 000000000000..20cbf0441822 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.fields.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [fields](./kibana-plugin-public.savedobjectsfindoptions.fields.md) + +## SavedObjectsFindOptions.fields property + +An array of fields to include in the results + +Signature: + +```typescript +fields?: string[]; +``` + +## Example + +SavedObjects.find({type: 'dashboard', fields: \['attributes.name', 'attributes.location'\]}) + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.hasreference.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.hasreference.md new file mode 100644 index 000000000000..63f65d22cc33 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.hasreference.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [hasReference](./kibana-plugin-public.savedobjectsfindoptions.hasreference.md) + +## SavedObjectsFindOptions.hasReference property + +Signature: + +```typescript +hasReference?: { + type: string; + id: string; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md new file mode 100644 index 000000000000..f90c60ebdd0d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) + +## SavedObjectsFindOptions interface + + +Signature: + +```typescript +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [defaultSearchOperator](./kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | +| [fields](./kibana-plugin-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | +| [hasReference](./kibana-plugin-public.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | +| [page](./kibana-plugin-public.savedobjectsfindoptions.page.md) | number | | +| [perPage](./kibana-plugin-public.savedobjectsfindoptions.perpage.md) | number | | +| [search](./kibana-plugin-public.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | +| [searchFields](./kibana-plugin-public.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | +| [sortField](./kibana-plugin-public.savedobjectsfindoptions.sortfield.md) | string | | +| [sortOrder](./kibana-plugin-public.savedobjectsfindoptions.sortorder.md) | string | | +| [type](./kibana-plugin-public.savedobjectsfindoptions.type.md) | string | string[] | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.page.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.page.md new file mode 100644 index 000000000000..982005adb245 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.page.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [page](./kibana-plugin-public.savedobjectsfindoptions.page.md) + +## SavedObjectsFindOptions.page property + +Signature: + +```typescript +page?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.perpage.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.perpage.md new file mode 100644 index 000000000000..3c61f690d82c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.perpage.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [perPage](./kibana-plugin-public.savedobjectsfindoptions.perpage.md) + +## SavedObjectsFindOptions.perPage property + +Signature: + +```typescript +perPage?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.search.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.search.md new file mode 100644 index 000000000000..f8f95e532982 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.search.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [search](./kibana-plugin-public.savedobjectsfindoptions.search.md) + +## SavedObjectsFindOptions.search property + +Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information + +Signature: + +```typescript +search?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.searchfields.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.searchfields.md new file mode 100644 index 000000000000..5e97ef00b441 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.searchfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [searchFields](./kibana-plugin-public.savedobjectsfindoptions.searchfields.md) + +## SavedObjectsFindOptions.searchFields property + +The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information + +Signature: + +```typescript +searchFields?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortfield.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortfield.md new file mode 100644 index 000000000000..14ab40894cec --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortfield.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [sortField](./kibana-plugin-public.savedobjectsfindoptions.sortfield.md) + +## SavedObjectsFindOptions.sortField property + +Signature: + +```typescript +sortField?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortorder.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortorder.md new file mode 100644 index 000000000000..b1e58658c008 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortorder.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [sortOrder](./kibana-plugin-public.savedobjectsfindoptions.sortorder.md) + +## SavedObjectsFindOptions.sortOrder property + +Signature: + +```typescript +sortOrder?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.type.md new file mode 100644 index 000000000000..6706e8344a1e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [type](./kibana-plugin-public.savedobjectsfindoptions.type.md) + +## SavedObjectsFindOptions.type property + +Signature: + +```typescript +type?: string | string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md new file mode 100644 index 000000000000..61a2daa59f16 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) + +## SavedObjectsFindResponsePublic interface + +Return type of the Saved Objects `find()` method. + +\*Note\*: this type is different between the Public and Server Saved Objects clients. + +Signature: + +```typescript +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [page](./kibana-plugin-public.savedobjectsfindresponsepublic.page.md) | number | | +| [perPage](./kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md) | number | | +| [total](./kibana-plugin-public.savedobjectsfindresponsepublic.total.md) | number | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.page.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.page.md new file mode 100644 index 000000000000..20e96d1e0df4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.page.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) > [page](./kibana-plugin-public.savedobjectsfindresponsepublic.page.md) + +## SavedObjectsFindResponsePublic.page property + +Signature: + +```typescript +page: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md new file mode 100644 index 000000000000..f706f9cb03b2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) > [perPage](./kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md) + +## SavedObjectsFindResponsePublic.perPage property + +Signature: + +```typescript +perPage: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.total.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.total.md new file mode 100644 index 000000000000..0a44c73436a2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.total.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) > [total](./kibana-plugin-public.savedobjectsfindresponsepublic.total.md) + +## SavedObjectsFindResponsePublic.total property + +Signature: + +```typescript +total: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsmigrationversion.md b/docs/development/core/public/kibana-plugin-public.savedobjectsmigrationversion.md new file mode 100644 index 000000000000..675adb9498c5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsmigrationversion.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) + +## SavedObjectsMigrationVersion interface + +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + +Signature: + +```typescript +export interface SavedObjectsMigrationVersion +``` + +## Example + +migrationVersion: { dashboard: '7.1.1', space: '6.6.6', } + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsstart.client.md b/docs/development/core/public/kibana-plugin-public.savedobjectsstart.client.md new file mode 100644 index 000000000000..d3e0da7a414b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsstart.client.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) > [client](./kibana-plugin-public.savedobjectsstart.client.md) + +## SavedObjectsStart.client property + +[SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) + +Signature: + +```typescript +client: SavedObjectsClientContract; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md b/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md new file mode 100644 index 000000000000..07a70f306cd2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) + +## SavedObjectsStart interface + + +Signature: + +```typescript +export interface SavedObjectsStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [client](./kibana-plugin-public.savedobjectsstart.client.md) | SavedObjectsClientContract | [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.md new file mode 100644 index 000000000000..800a78d65486 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) + +## SavedObjectsUpdateOptions interface + + +Signature: + +```typescript +export interface SavedObjectsUpdateOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [migrationVersion](./kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [references](./kibana-plugin-public.savedobjectsupdateoptions.references.md) | SavedObjectReference[] | | +| [version](./kibana-plugin-public.savedobjectsupdateoptions.version.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md new file mode 100644 index 000000000000..e5fe20acd399 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) > [migrationVersion](./kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md) + +## SavedObjectsUpdateOptions.migrationVersion property + +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.references.md b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.references.md new file mode 100644 index 000000000000..eda84ec8e0bf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) > [references](./kibana-plugin-public.savedobjectsupdateoptions.references.md) + +## SavedObjectsUpdateOptions.references property + +Signature: + +```typescript +references?: SavedObjectReference[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.version.md b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.version.md new file mode 100644 index 000000000000..9aacfa912401 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) > [version](./kibana-plugin-public.savedobjectsupdateoptions.version.md) + +## SavedObjectsUpdateOptions.version property + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.(constructor).md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.(constructor).md new file mode 100644 index 000000000000..e22154bc795f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.(constructor).md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [(constructor)](./kibana-plugin-public.simplesavedobject.(constructor).md) + +## SimpleSavedObject.(constructor) + +Constructs a new instance of the `SimpleSavedObject` class + +Signature: + +```typescript +constructor(client: SavedObjectsClient, { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| client | SavedObjectsClient | | +| { id, type, version, attributes, error, references, migrationVersion } | SavedObjectType<T> | | + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject._version.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject._version.md new file mode 100644 index 000000000000..7cbe08b8de76 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject._version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [\_version](./kibana-plugin-public.simplesavedobject._version.md) + +## SimpleSavedObject.\_version property + +Signature: + +```typescript +_version?: SavedObjectType['version']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.attributes.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.attributes.md new file mode 100644 index 000000000000..1c57136a1952 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.attributes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [attributes](./kibana-plugin-public.simplesavedobject.attributes.md) + +## SimpleSavedObject.attributes property + +Signature: + +```typescript +attributes: T; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.delete.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.delete.md new file mode 100644 index 000000000000..8a04acfedec6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.delete.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [delete](./kibana-plugin-public.simplesavedobject.delete.md) + +## SimpleSavedObject.delete() method + +Signature: + +```typescript +delete(): Promise<{}>; +``` +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.error.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.error.md new file mode 100644 index 000000000000..0b4f914ac92e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [error](./kibana-plugin-public.simplesavedobject.error.md) + +## SimpleSavedObject.error property + +Signature: + +```typescript +error: SavedObjectType['error']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.get.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.get.md new file mode 100644 index 000000000000..39a899e4a6cd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.get.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [get](./kibana-plugin-public.simplesavedobject.get.md) + +## SimpleSavedObject.get() method + +Signature: + +```typescript +get(key: string): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | string | | + +Returns: + +`any` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.has.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.has.md new file mode 100644 index 000000000000..5f3019d55c3f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.has.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [has](./kibana-plugin-public.simplesavedobject.has.md) + +## SimpleSavedObject.has() method + +Signature: + +```typescript +has(key: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.id.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.id.md new file mode 100644 index 000000000000..ed97976c4100 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [id](./kibana-plugin-public.simplesavedobject.id.md) + +## SimpleSavedObject.id property + +Signature: + +```typescript +id: SavedObjectType['id']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.md new file mode 100644 index 000000000000..a40a04c17a58 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.md @@ -0,0 +1,44 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) + +## SimpleSavedObject class + +This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md). + +It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. + +Signature: + +```typescript +export declare class SimpleSavedObject +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(client, { id, type, version, attributes, error, references, migrationVersion })](./kibana-plugin-public.simplesavedobject.(constructor).md) | | Constructs a new instance of the SimpleSavedObject class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [\_version](./kibana-plugin-public.simplesavedobject._version.md) | | SavedObjectType<T>['version'] | | +| [attributes](./kibana-plugin-public.simplesavedobject.attributes.md) | | T | | +| [error](./kibana-plugin-public.simplesavedobject.error.md) | | SavedObjectType<T>['error'] | | +| [id](./kibana-plugin-public.simplesavedobject.id.md) | | SavedObjectType<T>['id'] | | +| [migrationVersion](./kibana-plugin-public.simplesavedobject.migrationversion.md) | | SavedObjectType<T>['migrationVersion'] | | +| [references](./kibana-plugin-public.simplesavedobject.references.md) | | SavedObjectType<T>['references'] | | +| [type](./kibana-plugin-public.simplesavedobject.type.md) | | SavedObjectType<T>['type'] | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [delete()](./kibana-plugin-public.simplesavedobject.delete.md) | | | +| [get(key)](./kibana-plugin-public.simplesavedobject.get.md) | | | +| [has(key)](./kibana-plugin-public.simplesavedobject.has.md) | | | +| [save()](./kibana-plugin-public.simplesavedobject.save.md) | | | +| [set(key, value)](./kibana-plugin-public.simplesavedobject.set.md) | | | + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.migrationversion.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.migrationversion.md new file mode 100644 index 000000000000..6f7b3af03099 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.migrationversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [migrationVersion](./kibana-plugin-public.simplesavedobject.migrationversion.md) + +## SimpleSavedObject.migrationVersion property + +Signature: + +```typescript +migrationVersion: SavedObjectType['migrationVersion']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.references.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.references.md new file mode 100644 index 000000000000..159f855538f6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [references](./kibana-plugin-public.simplesavedobject.references.md) + +## SimpleSavedObject.references property + +Signature: + +```typescript +references: SavedObjectType['references']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.save.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.save.md new file mode 100644 index 000000000000..05f8880fbcdd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.save.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [save](./kibana-plugin-public.simplesavedobject.save.md) + +## SimpleSavedObject.save() method + +Signature: + +```typescript +save(): Promise>; +``` +Returns: + +`Promise>` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.set.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.set.md new file mode 100644 index 000000000000..ce3f9c5919d7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.set.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [set](./kibana-plugin-public.simplesavedobject.set.md) + +## SimpleSavedObject.set() method + +Signature: + +```typescript +set(key: string, value: any): T; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | string | | +| value | any | | + +Returns: + +`T` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.type.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.type.md new file mode 100644 index 000000000000..b004c70013d6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [type](./kibana-plugin-public.simplesavedobject.type.md) + +## SimpleSavedObject.type property + +Signature: + +```typescript +type: SavedObjectType['type']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md index 1437d5083df2..ced5cfd9413e 100644 --- a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md +++ b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type AuthenticationHandler = (request: KibanaRequest, t: AuthToolkit) => AuthResult | Promise; +export declare type AuthenticationHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: AuthToolkit) => AuthResult | KibanaResponse | Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authheaders.md b/docs/development/core/server/kibana-plugin-server.authheaders.md index 96939cb8bbcb..bdb7cda2fa30 100644 --- a/docs/development/core/server/kibana-plugin-server.authheaders.md +++ b/docs/development/core/server/kibana-plugin-server.authheaders.md @@ -9,5 +9,5 @@ Auth Headers map Signature: ```typescript -export declare type AuthHeaders = Record; +export declare type AuthHeaders = Record; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authresultdata.headers.md b/docs/development/core/server/kibana-plugin-server.authresultdata.headers.md deleted file mode 100644 index 4287978c3ac3..000000000000 --- a/docs/development/core/server/kibana-plugin-server.authresultdata.headers.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultData](./kibana-plugin-server.authresultdata.md) > [headers](./kibana-plugin-server.authresultdata.headers.md) - -## AuthResultData.headers property - -Auth specific headers to authenticate a user against Elasticsearch. - -Signature: - -```typescript -headers: AuthHeaders; -``` diff --git a/docs/development/core/server/kibana-plugin-server.authresultdata.md b/docs/development/core/server/kibana-plugin-server.authresultdata.md deleted file mode 100644 index 57908bd70459..000000000000 --- a/docs/development/core/server/kibana-plugin-server.authresultdata.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultData](./kibana-plugin-server.authresultdata.md) - -## AuthResultData interface - -Result of an incoming request authentication. - -Signature: - -```typescript -export interface AuthResultData -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [headers](./kibana-plugin-server.authresultdata.headers.md) | AuthHeaders | Auth specific headers to authenticate a user against Elasticsearch. | -| [state](./kibana-plugin-server.authresultdata.state.md) | Record<string, any> | Data to associate with an incoming request. Any downstream plugin may get access to the data. | - diff --git a/docs/development/core/server/kibana-plugin-server.authresultdata.state.md b/docs/development/core/server/kibana-plugin-server.authresultdata.state.md deleted file mode 100644 index 70054395514b..000000000000 --- a/docs/development/core/server/kibana-plugin-server.authresultdata.state.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultData](./kibana-plugin-server.authresultdata.md) > [state](./kibana-plugin-server.authresultdata.state.md) - -## AuthResultData.state property - -Data to associate with an incoming request. Any downstream plugin may get access to the data. - -Signature: - -```typescript -state: Record; -``` diff --git a/docs/development/core/server/kibana-plugin-server.authresultparams.md b/docs/development/core/server/kibana-plugin-server.authresultparams.md new file mode 100644 index 000000000000..b098fe278d85 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authresultparams.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultParams](./kibana-plugin-server.authresultparams.md) + +## AuthResultParams interface + +Result of an incoming request authentication. + +Signature: + +```typescript +export interface AuthResultParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [requestHeaders](./kibana-plugin-server.authresultparams.requestheaders.md) | AuthHeaders | Auth specific headers to attach to a request object. Used to perform a request to Elasticsearch on behalf of an authenticated user. | +| [responseHeaders](./kibana-plugin-server.authresultparams.responseheaders.md) | AuthHeaders | Auth specific headers to attach to a response object. Used to send back authentication mechanism related headers to a client when needed. | +| [state](./kibana-plugin-server.authresultparams.state.md) | Record<string, any> | Data to associate with an incoming request. Any downstream plugin may get access to the data. | + diff --git a/docs/development/core/server/kibana-plugin-server.authresultparams.requestheaders.md b/docs/development/core/server/kibana-plugin-server.authresultparams.requestheaders.md new file mode 100644 index 000000000000..0fda032b64f9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authresultparams.requestheaders.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultParams](./kibana-plugin-server.authresultparams.md) > [requestHeaders](./kibana-plugin-server.authresultparams.requestheaders.md) + +## AuthResultParams.requestHeaders property + +Auth specific headers to attach to a request object. Used to perform a request to Elasticsearch on behalf of an authenticated user. + +Signature: + +```typescript +requestHeaders?: AuthHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authresultparams.responseheaders.md b/docs/development/core/server/kibana-plugin-server.authresultparams.responseheaders.md new file mode 100644 index 000000000000..c14feb25801d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authresultparams.responseheaders.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultParams](./kibana-plugin-server.authresultparams.md) > [responseHeaders](./kibana-plugin-server.authresultparams.responseheaders.md) + +## AuthResultParams.responseHeaders property + +Auth specific headers to attach to a response object. Used to send back authentication mechanism related headers to a client when needed. + +Signature: + +```typescript +responseHeaders?: AuthHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authresultparams.state.md b/docs/development/core/server/kibana-plugin-server.authresultparams.state.md new file mode 100644 index 000000000000..8ca3da20a9c2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authresultparams.state.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultParams](./kibana-plugin-server.authresultparams.md) > [state](./kibana-plugin-server.authresultparams.state.md) + +## AuthResultParams.state property + +Data to associate with an incoming request. Any downstream plugin may get access to the data. + +Signature: + +```typescript +state?: Record; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authstatus.md b/docs/development/core/server/kibana-plugin-server.authstatus.md new file mode 100644 index 000000000000..e59ade4f73e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authstatus.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthStatus](./kibana-plugin-server.authstatus.md) + +## AuthStatus enum + +Status indicating an outcome of the authentication. + +Signature: + +```typescript +export declare enum AuthStatus +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| authenticated | "authenticated" | auth interceptor successfully authenticated a user | +| unauthenticated | "unauthenticated" | auth interceptor failed user authentication | +| unknown | "unknown" | auth interceptor has not been registered | + diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md index e8e245ac0159..54d78c840ed5 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md @@ -9,5 +9,5 @@ Authentication is successful with given credentials, allow request to pass throu Signature: ```typescript -authenticated: (data?: Partial) => AuthResult; +authenticated: (data?: AuthResultParams) => AuthResult; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.md index 2fe4312153a6..0c030ddce4ec 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.md @@ -16,7 +16,5 @@ export interface AuthToolkit | Property | Type | Description | | --- | --- | --- | -| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (data?: Partial<AuthResultData>) => AuthResult | Authentication is successful with given credentials, allow request to pass through | -| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | (url: string) => AuthResult | Authentication requires to interrupt request handling and redirect to a configured url | -| [rejected](./kibana-plugin-server.authtoolkit.rejected.md) | (error: Error, options?: {
statusCode?: number;
}) => AuthResult | Authentication is unsuccessful, fail the request with specified error. | +| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (data?: AuthResultParams) => AuthResult | Authentication is successful with given credentials, allow request to pass through | diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md deleted file mode 100644 index eb07b1c4b0f6..000000000000 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [redirected](./kibana-plugin-server.authtoolkit.redirected.md) - -## AuthToolkit.redirected property - -Authentication requires to interrupt request handling and redirect to a configured url - -Signature: - -```typescript -redirected: (url: string) => AuthResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md deleted file mode 100644 index bc353c7df9fb..000000000000 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [rejected](./kibana-plugin-server.authtoolkit.rejected.md) - -## AuthToolkit.rejected property - -Authentication is unsuccessful, fail the request with specified error. - -Signature: - -```typescript -rejected: (error: Error, options?: { - statusCode?: number; - }) => AuthResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.configpath.md b/docs/development/core/server/kibana-plugin-server.configpath.md new file mode 100644 index 000000000000..674769115b18 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configpath.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigPath](./kibana-plugin-server.configpath.md) + +## ConfigPath type + + +Signature: + +```typescript +export declare type ConfigPath = string | string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md b/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md new file mode 100644 index 000000000000..e028e1391f28 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ContextSetup](./kibana-plugin-server.contextsetup.md) > [createContextContainer](./kibana-plugin-server.contextsetup.createcontextcontainer.md) + +## ContextSetup.createContextContainer() method + +Creates a new for a service owner. + +Signature: + +```typescript +createContextContainer(): IContextContainer; +``` +Returns: + +`IContextContainer` + diff --git a/docs/development/core/server/kibana-plugin-server.contextsetup.md b/docs/development/core/server/kibana-plugin-server.contextsetup.md new file mode 100644 index 000000000000..972266583e33 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.contextsetup.md @@ -0,0 +1,78 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ContextSetup](./kibana-plugin-server.contextsetup.md) + +## ContextSetup interface + +Signature: + +```typescript +export interface ContextSetup +``` + +## Methods + +| Method | Description | +| --- | --- | +| [createContextContainer()](./kibana-plugin-server.contextsetup.createcontextcontainer.md) | Creates a new for a service owner. | + +## Example + +Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. + +```ts +export interface VizRenderContext { + core: { + i18n: I18nStart; + uiSettings: UISettingsClientContract; + } + [contextName: string]: unknown; +} + +export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; + +class VizRenderingPlugin { + private readonly vizRenderers = new Map () => void)>(); + + constructor(private readonly initContext: PluginInitializerContext) {} + + setup(core) { + this.contextContainer = core.context.createContextContainer< + VizRenderContext, + ReturnType, + [HTMLElement] + >(); + + return { + registerContext: this.contextContainer.registerContext, + registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) => + this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)), + }; + } + + start(core) { + // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. + this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ + i18n: core.i18n, + uiSettings: core.uiSettings + })); + + return { + registerContext: this.contextContainer.registerContext, + + renderVizualization: (renderMethod: string, domElement: HTMLElement) => { + if (!this.vizRenderer.has(renderMethod)) { + throw new Error(`Render method '${renderMethod}' has not been registered`); + } + + // The handler can now be called directly with only an `HTMLElement` and will automatically + // have a new `context` object created and populated by the context container. + const handler = this.vizRenderers.get(renderMethod) + return handler(domElement); + } + }; + } +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.context.md b/docs/development/core/server/kibana-plugin-server.coresetup.context.md new file mode 100644 index 000000000000..e98cd6a0d04e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.coresetup.context.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [context](./kibana-plugin-server.coresetup.context.md) + +## CoreSetup.context property + +Signature: + +```typescript +context: { + createContextContainer: ContextSetup['createContextContainer']; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-server.coresetup.http.md index c9206b7a7e71..e5347dd7c662 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.http.md @@ -13,7 +13,6 @@ http: { registerAuth: HttpServiceSetup['registerAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; basePath: HttpServiceSetup['basePath']; - createNewServer: HttpServiceSetup['createNewServer']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index f4653d7f4357..8af0c6e62fb5 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -16,6 +16,7 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | +| [context](./kibana-plugin-server.coresetup.context.md) | {
createContextContainer: ContextSetup['createContextContainer'];
} | | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {
adminClient$: Observable<ClusterClient>;
dataClient$: Observable<ClusterClient>;
createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ClusterClient;
} | | -| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
} | | +| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
} | | diff --git a/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.md b/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.md new file mode 100644 index 000000000000..cabee8a47e5c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CustomHttpResponseOptions](./kibana-plugin-server.customhttpresponseoptions.md) + +## CustomHttpResponseOptions interface + +HTTP response parameters for a response with adjustable status code. + +Signature: + +```typescript +export interface CustomHttpResponseOptions extends HttpResponseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [statusCode](./kibana-plugin-server.customhttpresponseoptions.statuscode.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.statuscode.md b/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.statuscode.md new file mode 100644 index 000000000000..5444ccd2ebb5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.customhttpresponseoptions.statuscode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CustomHttpResponseOptions](./kibana-plugin-server.customhttpresponseoptions.md) > [statusCode](./kibana-plugin-server.customhttpresponseoptions.statuscode.md) + +## CustomHttpResponseOptions.statusCode property + +Signature: + +```typescript +statusCode: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.getauthheaders.md b/docs/development/core/server/kibana-plugin-server.getauthheaders.md index ee7572615fe1..fba8b8ca8ee3 100644 --- a/docs/development/core/server/kibana-plugin-server.getauthheaders.md +++ b/docs/development/core/server/kibana-plugin-server.getauthheaders.md @@ -9,5 +9,5 @@ Get headers to authenticate a user against Elasticsearch. Signature: ```typescript -export declare type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; +export declare type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined; ``` diff --git a/docs/development/core/server/kibana-plugin-server.getauthstate.md b/docs/development/core/server/kibana-plugin-server.getauthstate.md new file mode 100644 index 000000000000..47fc38c28f5e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.getauthstate.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [GetAuthState](./kibana-plugin-server.getauthstate.md) + +## GetAuthState type + +Get authentication state for a request. Returned by `auth` interceptor. + +Signature: + +```typescript +export declare type GetAuthState = (request: KibanaRequest | LegacyRequest) => { + status: AuthStatus; + state: unknown; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-server.headers.md b/docs/development/core/server/kibana-plugin-server.headers.md index 83259efe8b79..cd73d4de43b9 100644 --- a/docs/development/core/server/kibana-plugin-server.headers.md +++ b/docs/development/core/server/kibana-plugin-server.headers.md @@ -4,9 +4,14 @@ ## Headers type +Http request headers to read. Signature: ```typescript -export declare type Headers = Record; +export declare type Headers = { + [header in KnownHeaders]?: string | string[] | undefined; +} & { + [header: string]: string | string[] | undefined; +}; ``` diff --git a/docs/development/core/server/kibana-plugin-server.httpresponseoptions.headers.md b/docs/development/core/server/kibana-plugin-server.httpresponseoptions.headers.md new file mode 100644 index 000000000000..ee347f99a41a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpresponseoptions.headers.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpResponseOptions](./kibana-plugin-server.httpresponseoptions.md) > [headers](./kibana-plugin-server.httpresponseoptions.headers.md) + +## HttpResponseOptions.headers property + +HTTP Headers with additional information about response + +Signature: + +```typescript +headers?: ResponseHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpresponseoptions.md b/docs/development/core/server/kibana-plugin-server.httpresponseoptions.md new file mode 100644 index 000000000000..8f9ccf22c8c6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpresponseoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpResponseOptions](./kibana-plugin-server.httpresponseoptions.md) + +## HttpResponseOptions interface + +HTTP response parameters + +Signature: + +```typescript +export interface HttpResponseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-server.httpresponseoptions.headers.md) | ResponseHeaders | HTTP Headers with additional information about response | + diff --git a/docs/development/core/server/kibana-plugin-server.httpresponsepayload.md b/docs/development/core/server/kibana-plugin-server.httpresponsepayload.md new file mode 100644 index 000000000000..3dc4e2c7956f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpresponsepayload.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpResponsePayload](./kibana-plugin-server.httpresponsepayload.md) + +## HttpResponsePayload type + +Data send to the client as a response payload. + +Signature: + +```typescript +export declare type HttpResponsePayload = undefined | string | Record | Buffer | Stream; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.auth.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.auth.md new file mode 100644 index 000000000000..e39c3c631676 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.auth.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [auth](./kibana-plugin-server.httpserversetup.auth.md) + +## HttpServerSetup.auth property + +Signature: + +```typescript +auth: { + get: GetAuthState; + isAuthenticated: IsAuthenticated; + getAuthHeaders: GetAuthHeaders; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md new file mode 100644 index 000000000000..5cfb2f5c4e8b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [basePath](./kibana-plugin-server.httpserversetup.basepath.md) + +## HttpServerSetup.basePath property + +Signature: + +```typescript +basePath: { + get: (request: KibanaRequest | LegacyRequest) => string; + set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; + prepend: (url: string) => string; + remove: (url: string) => string; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md new file mode 100644 index 000000000000..3dc01a52a2f5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [createCookieSessionStorageFactory](./kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md) + +## HttpServerSetup.createCookieSessionStorageFactory property + +Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) + +Signature: + +```typescript +createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.istlsenabled.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.istlsenabled.md new file mode 100644 index 000000000000..6961d4feeb7c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.istlsenabled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [isTlsEnabled](./kibana-plugin-server.httpserversetup.istlsenabled.md) + +## HttpServerSetup.isTlsEnabled property + +Flag showing whether a server was configured to use TLS connection. + +Signature: + +```typescript +isTlsEnabled: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.md new file mode 100644 index 000000000000..143ae66c0b32 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.md @@ -0,0 +1,93 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) + +## HttpServerSetup interface + +Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to `hapi` server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. + +Signature: + +```typescript +export interface HttpServerSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [auth](./kibana-plugin-server.httpserversetup.auth.md) | {
get: GetAuthState;
isAuthenticated: IsAuthenticated;
getAuthHeaders: GetAuthHeaders;
} | | +| [basePath](./kibana-plugin-server.httpserversetup.basepath.md) | {
get: (request: KibanaRequest | LegacyRequest) => string;
set: (request: KibanaRequest | LegacyRequest, basePath: string) => void;
prepend: (url: string) => string;
remove: (url: string) => string;
} | | +| [createCookieSessionStorageFactory](./kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | +| [isTlsEnabled](./kibana-plugin-server.httpserversetup.istlsenabled.md) | boolean | Flag showing whether a server was configured to use TLS connection. | +| [registerAuth](./kibana-plugin-server.httpserversetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. A handler should return a state to associate with the incoming request. The state can be retrieved later via http.auth.get(..) Only one AuthenticationHandler can be registered. | +| [registerOnPostAuth](./kibana-plugin-server.httpserversetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. Runs the handler after Auth interceptor did make sure a user has access to the requested resource. The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPreAuth, which are called in sequence (from the first registered to the last). | +| [registerOnPreAuth](./kibana-plugin-server.httpserversetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests. Runs the handler before Auth interceptor performs a check that user has access to requested resources, so it's the only place when you can forward a request to another URL right on the server. Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). | +| [registerRouter](./kibana-plugin-server.httpserversetup.registerrouter.md) | (router: Router) => void | Add all the routes registered with router to HTTP server request listeners. | +| [server](./kibana-plugin-server.httpserversetup.server.md) | Server | | + +## Example + +To handle an incoming request in your plugin you should: - Create a `Router` instance. Use `plugin-id` as a prefix path segment for your routes. + +```ts +import { Router } from 'src/core/server'; +const router = new Router('my-app'); + +``` +- Use `@kbn/config-schema` package to create a schema to validate the request `params`, `query`, and `body`. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error without calling the route's handler. To opt out of validating the request, specify `false`. + +```ts +import { schema, TypeOf } from '@kbn/config-schema'; +const validate = { + params: schema.object({ + id: schema.string(), + }), +}; + +``` +- Declare a function to respond to incoming request. The function will receive `request` object containing request details: url, headers, matched route, as well as validated `params`, `query`, `body`. And `response` object instructing HTTP server to create HTTP response with information sent back to the client as the response body, headers, and HTTP status. Unlike, `hapi` route handler in the Legacy platform, any exception raised during the handler call will generate `500 Server error` response and log error details for further investigation. See below for returning custom error responses. + +```ts +const handler = async (request: KibanaRequest, response: ResponseFactory) => { + const data = await findObject(request.params.id); + // creates a command to respond with 'not found' error + if (!data) return response.notFound(); + // creates a command to send found data to the client and set response headers + return response.ok(data, { + headers: { + 'content-type': 'application/json' + } + }); +} + +``` +- Register route handler for GET request to 'my-app/path/{id}' path + +```ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { Router } from 'src/core/server'; +const router = new Router('my-app'); + +const validate = { + params: schema.object({ + id: schema.string(), + }), +}; + +router.get({ + path: 'path/{id}', + validate +}, +async (request, response) => { + const data = await findObject(request.params.id); + if (!data) return response.notFound(); + return response.ok(data, { + headers: { + 'content-type': 'application/json' + } + }); +}); + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.registerauth.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerauth.md new file mode 100644 index 000000000000..6e63e0996a63 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerauth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [registerAuth](./kibana-plugin-server.httpserversetup.registerauth.md) + +## HttpServerSetup.registerAuth property + +To define custom authentication and/or authorization mechanism for incoming requests. A handler should return a state to associate with the incoming request. The state can be retrieved later via http.auth.get(..) Only one AuthenticationHandler can be registered. + +Signature: + +```typescript +registerAuth: (handler: AuthenticationHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpostauth.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpostauth.md new file mode 100644 index 000000000000..c74a67da350e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpostauth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [registerOnPostAuth](./kibana-plugin-server.httpserversetup.registeronpostauth.md) + +## HttpServerSetup.registerOnPostAuth property + +To define custom logic to perform for incoming requests. Runs the handler after Auth interceptor did make sure a user has access to the requested resource. The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPreAuth, which are called in sequence (from the first registered to the last). + +Signature: + +```typescript +registerOnPostAuth: (handler: OnPostAuthHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpreauth.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpreauth.md new file mode 100644 index 000000000000..f6efa1c1dd73 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.registeronpreauth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [registerOnPreAuth](./kibana-plugin-server.httpserversetup.registeronpreauth.md) + +## HttpServerSetup.registerOnPreAuth property + +To define custom logic to perform for incoming requests. Runs the handler before Auth interceptor performs a check that user has access to requested resources, so it's the only place when you can forward a request to another URL right on the server. Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). + +Signature: + +```typescript +registerOnPreAuth: (handler: OnPreAuthHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md new file mode 100644 index 000000000000..4c2a9ae32740 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [registerRouter](./kibana-plugin-server.httpserversetup.registerrouter.md) + +## HttpServerSetup.registerRouter property + +Add all the routes registered with `router` to HTTP server request listeners. + +Signature: + +```typescript +registerRouter: (router: Router) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.server.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.server.md new file mode 100644 index 000000000000..a137eba7c8a5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.server.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) > [server](./kibana-plugin-server.httpserversetup.server.md) + +## HttpServerSetup.server property + +Signature: + +```typescript +server: Server; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md deleted file mode 100644 index e41684ea2b78..000000000000 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.createnewserver.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [createNewServer](./kibana-plugin-server.httpservicesetup.createnewserver.md) - -## HttpServiceSetup.createNewServer property - -Signature: - -```typescript -createNewServer: (cfg: Partial) => Promise; -``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index ec4a2537b840..7e8f17510c8e 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -2,18 +2,11 @@ [Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) -## HttpServiceSetup interface +## HttpServiceSetup type Signature: ```typescript -export interface HttpServiceSetup extends HttpServerSetup +export declare type HttpServiceSetup = HttpServerSetup; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [createNewServer](./kibana-plugin-server.httpservicesetup.createnewserver.md) | (cfg: Partial<HttpConfig>) => Promise<HttpServerSetup> | | - diff --git a/docs/development/core/server/kibana-plugin-server.ikibanasocket.getpeercertificate_1.md b/docs/development/core/server/kibana-plugin-server.ikibanasocket.getpeercertificate_1.md new file mode 100644 index 000000000000..0c6cb0ce8bb0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.ikibanasocket.getpeercertificate_1.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) > [getPeerCertificate](./kibana-plugin-server.ikibanasocket.getpeercertificate_1.md) + +## IKibanaSocket.getPeerCertificate() method + +Signature: + +```typescript +getPeerCertificate(detailed: true): DetailedPeerCertificate | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| detailed | true | | + +Returns: + +`DetailedPeerCertificate | null` + diff --git a/docs/development/core/server/kibana-plugin-server.ikibanasocket.getpeercertificate_2.md b/docs/development/core/server/kibana-plugin-server.ikibanasocket.getpeercertificate_2.md new file mode 100644 index 000000000000..a7c63b9d3047 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.ikibanasocket.getpeercertificate_2.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) > [getPeerCertificate](./kibana-plugin-server.ikibanasocket.getpeercertificate_2.md) + +## IKibanaSocket.getPeerCertificate() method + +Signature: + +```typescript +getPeerCertificate(detailed: false): PeerCertificate | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| detailed | false | | + +Returns: + +`PeerCertificate | null` + diff --git a/docs/development/core/server/kibana-plugin-server.ikibanasocket.getpeercertificate_3.md b/docs/development/core/server/kibana-plugin-server.ikibanasocket.getpeercertificate_3.md new file mode 100644 index 000000000000..0c81a26cf15e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.ikibanasocket.getpeercertificate_3.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) > [getPeerCertificate](./kibana-plugin-server.ikibanasocket.getpeercertificate_3.md) + +## IKibanaSocket.getPeerCertificate() method + +Returns an object representing the peer's certificate. The returned object has some properties corresponding to the field of the certificate. If detailed argument is true the full chain with issuer property will be returned, if false only the top certificate without issuer property. If the peer does not provide a certificate, it returns null. + +Signature: + +```typescript +getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| detailed | boolean | If true; the full chain with issuer property will be returned. | + +Returns: + +`PeerCertificate | DetailedPeerCertificate | null` + +An object representing the peer's certificate. + diff --git a/docs/development/core/server/kibana-plugin-server.ikibanasocket.md b/docs/development/core/server/kibana-plugin-server.ikibanasocket.md new file mode 100644 index 000000000000..129a3b1d2311 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.ikibanasocket.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) + +## IKibanaSocket interface + +A tiny abstraction for TCP socket. + +Signature: + +```typescript +export interface IKibanaSocket +``` + +## Methods + +| Method | Description | +| --- | --- | +| [getPeerCertificate(detailed)](./kibana-plugin-server.ikibanasocket.getpeercertificate_1.md) | | +| [getPeerCertificate(detailed)](./kibana-plugin-server.ikibanasocket.getpeercertificate_2.md) | | +| [getPeerCertificate(detailed)](./kibana-plugin-server.ikibanasocket.getpeercertificate_3.md) | Returns an object representing the peer's certificate. The returned object has some properties corresponding to the field of the certificate. If detailed argument is true the full chain with issuer property will be returned, if false only the top certificate without issuer property. If the peer does not provide a certificate, it returns null. | + diff --git a/docs/development/core/server/kibana-plugin-server.isauthenticated.md b/docs/development/core/server/kibana-plugin-server.isauthenticated.md new file mode 100644 index 000000000000..15f412710412 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.isauthenticated.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) + +## IsAuthenticated type + +Return authentication status for a request. + +Signature: + +```typescript +export declare type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index a9622e4319d5..6faa9606a843 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -26,6 +26,7 @@ export declare class KibanaRequestHeaders | Readonly copy of incoming request headers. | | [params](./kibana-plugin-server.kibanarequest.params.md) | | Params | | | [query](./kibana-plugin-server.kibanarequest.query.md) | | Query | | -| [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute> | | -| [url](./kibana-plugin-server.kibanarequest.url.md) | | Url | | +| [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute> | matched route details | +| [socket](./kibana-plugin-server.kibanarequest.socket.md) | | IKibanaSocket | | +| [url](./kibana-plugin-server.kibanarequest.url.md) | | Url | a WHATWG URL standard object. | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md index 301eeef1b6bb..88954eedf4cf 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md @@ -4,6 +4,8 @@ ## KibanaRequest.route property +matched route details + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.socket.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.socket.md new file mode 100644 index 000000000000..3880428273ac --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.socket.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [socket](./kibana-plugin-server.kibanarequest.socket.md) + +## KibanaRequest.socket property + +Signature: + +```typescript +readonly socket: IKibanaSocket; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md index b8bd46199763..62d1f9715947 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md @@ -4,6 +4,8 @@ ## KibanaRequest.url property +a WHATWG URL standard object. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md new file mode 100644 index 000000000000..82832ee9334a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) + +## KibanaResponseFactory type + +Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. + +Signature: + +```typescript +export declare type KibanaResponseFactory = typeof kibanaResponseFactory; +``` diff --git a/docs/development/core/server/kibana-plugin-server.knownheaders.md b/docs/development/core/server/kibana-plugin-server.knownheaders.md new file mode 100644 index 000000000000..986794f3aaa6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.knownheaders.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KnownHeaders](./kibana-plugin-server.knownheaders.md) + +## KnownHeaders type + +Set of well-known HTTP headers. + +Signature: + +```typescript +export declare type KnownHeaders = KnownKeys; +``` diff --git a/docs/development/core/server/kibana-plugin-server.legacyrequest.md b/docs/development/core/server/kibana-plugin-server.legacyrequest.md index 6f67928faa52..a794b3bbe87c 100644 --- a/docs/development/core/server/kibana-plugin-server.legacyrequest.md +++ b/docs/development/core/server/kibana-plugin-server.legacyrequest.md @@ -2,12 +2,15 @@ [Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [LegacyRequest](./kibana-plugin-server.legacyrequest.md) -## LegacyRequest type +## LegacyRequest interface -Support Legacy platform request for the period of migration. +> Warning: This API is now obsolete. +> +> `hapi` request object, supported during migration process only for backward compatibility. +> Signature: ```typescript -export declare type LegacyRequest = Request; +export interface LegacyRequest extends Request ``` diff --git a/docs/development/core/server/kibana-plugin-server.lifecycleresponsefactory.md b/docs/development/core/server/kibana-plugin-server.lifecycleresponsefactory.md new file mode 100644 index 000000000000..f1c427203dd2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.lifecycleresponsefactory.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [LifecycleResponseFactory](./kibana-plugin-server.lifecycleresponsefactory.md) + +## LifecycleResponseFactory type + +Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. + +Signature: + +```typescript +export declare type LifecycleResponseFactory = typeof lifecycleResponseFactory; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index ab79f2b38290..558ff343b02d 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -6,6 +6,8 @@ The Kibana Core APIs for server-side plugins. +A plugin requires a `kibana.json` file at it's root directory that follows [the manfiest schema](./kibana-plugin-server.pluginmanifest.md) to define static plugin information required to load the plugin. + A plugin's `server/index` file must contain a named import, `plugin`, that implements [PluginInitializer](./kibana-plugin-server.plugininitializer.md) which returns an object that implements [Plugin](./kibana-plugin-server.plugin.md). The plugin integrates with the core system via lifecycle events: `setup`, `start`, and `stop`. In each lifecycle method, the plugin will receive the corresponding core services available (either [CoreSetup](./kibana-plugin-server.coresetup.md) or [CoreStart](./kibana-plugin-server.corestart.md)) and any interfaces returned by dependency plugins' lifecycle method. Anything returned by the plugin's lifecycle method will be exposed to downstream dependencies when their corresponding lifecycle methods are invoked. @@ -17,27 +19,40 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | -| [Router](./kibana-plugin-server.router.md) | | +| [Router](./kibana-plugin-server.router.md) | Provides ability to declare a handler function for a particular path and HTTP request method. Each route can have only one handler functions, which is executed when the route is matched. | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | +| [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) | | +| [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) | | | [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API | +## Enumerations + +| Enumeration | Description | +| --- | --- | +| [AuthStatus](./kibana-plugin-server.authstatus.md) | Status indicating an outcome of the authentication. | + ## Interfaces | Interface | Description | | --- | --- | -| [AuthResultData](./kibana-plugin-server.authresultdata.md) | Result of an incoming request authentication. | +| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of an incoming request authentication. | | [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | +| [ContextSetup](./kibana-plugin-server.contextsetup.md) | | | [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins setup method. | | [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start method. | +| [CustomHttpResponseOptions](./kibana-plugin-server.customhttpresponseoptions.md) | HTTP response parameters for a response with adjustable status code. | | [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | | [ElasticsearchError](./kibana-plugin-server.elasticsearcherror.md) | | | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | | [FakeRequest](./kibana-plugin-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | -| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | +| [HttpResponseOptions](./kibana-plugin-server.httpresponseoptions.md) | HTTP response parameters | +| [HttpServerSetup](./kibana-plugin-server.httpserversetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | +| [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) | | | [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | +| [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | | | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | | [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | @@ -45,27 +60,50 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | +| [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | | [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | -| [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Route specific configuration. | +| [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) | Additional metadata to enhance error output or provide error details. | +| [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. | +| [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. | | [SavedObject](./kibana-plugin-server.savedobject.md) | | -| [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | | +| [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | The data for a Saved Object is stored in the attributes key as either an object or an array of objects. | | [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) | A reference to another saved object. | | [SavedObjectsBaseOptions](./kibana-plugin-server.savedobjectsbaseoptions.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) | | | [SavedObjectsBulkGetObject](./kibana-plugin-server.savedobjectsbulkgetobject.md) | | | [SavedObjectsBulkResponse](./kibana-plugin-server.savedobjectsbulkresponse.md) | | -| [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | | +| [SavedObjectsClientProviderOptions](./kibana-plugin-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | +| [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | | [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | | +| [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) | Options controlling the export operation. | | [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) | | -| [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) | | -| [SavedObjectsMigrationVersion](./kibana-plugin-server.savedobjectsmigrationversion.md) | A dictionary of saved object type -> version used to determine what migrations need to be applied to a saved object. | +| [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsImportConflictError](./kibana-plugin-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | +| [SavedObjectsImportError](./kibana-plugin-server.savedobjectsimporterror.md) | Represents a failure to import. | +| [SavedObjectsImportMissingReferencesError](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | +| [SavedObjectsImportOptions](./kibana-plugin-server.savedobjectsimportoptions.md) | Options to control the import operation. | +| [SavedObjectsImportResponse](./kibana-plugin-server.savedobjectsimportresponse.md) | The response describing the result of an import. | +| [SavedObjectsImportRetry](./kibana-plugin-server.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | +| [SavedObjectsImportUnknownError](./kibana-plugin-server.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | +| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-server.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | +| [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) | | +| [SavedObjectsMigrationVersion](./kibana-plugin-server.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. | +| [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | | [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) | | | [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) | | | [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | +| [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) | Configuration used to create HTTP session storage based on top of cookie mechanism. | | [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | +## Variables + +| Variable | Description | +| --- | --- | +| [kibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) | Set of helpers used to create KibanaResponse to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-server.requesthandler.md) execution. | + ## Type Aliases | Type Alias | Description | @@ -73,16 +111,28 @@ The plugin integrates with the core system via lifecycle events: `setup` | [APICaller](./kibana-plugin-server.apicaller.md) | | | [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | | | [AuthHeaders](./kibana-plugin-server.authheaders.md) | Auth Headers map | +| [ConfigPath](./kibana-plugin-server.configpath.md) | | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | -| [Headers](./kibana-plugin-server.headers.md) | | -| [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | Support Legacy platform request for the period of migration. | +| [GetAuthState](./kibana-plugin-server.getauthstate.md) | Get authentication state for a request. Returned by auth interceptor. | +| [Headers](./kibana-plugin-server.headers.md) | Http request headers to read. | +| [HttpResponsePayload](./kibana-plugin-server.httpresponsepayload.md) | Data send to the client as a response payload. | +| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | +| [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. | +| [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. | +| [KnownHeaders](./kibana-plugin-server.knownheaders.md) | Set of well-known HTTP headers. | +| [LifecycleResponseFactory](./kibana-plugin-server.lifecycleresponsefactory.md) | Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | +| [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md) | | | [RecursiveReadonly](./kibana-plugin-server.recursivereadonly.md) | | +| [RedirectResponseOptions](./kibana-plugin-server.redirectresponseoptions.md) | HTTP response parameters for redirection response | +| [RequestHandler](./kibana-plugin-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) functions. | +| [ResponseError](./kibana-plugin-server.responseerror.md) | Error message and optional data send to the client in case of error. | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | -| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | \#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | -| [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | | +| [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | +| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | +| [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md b/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md index b28f55233d54..884eb3e6346b 100644 --- a/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md +++ b/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type OnPostAuthHandler = (request: KibanaRequest, t: OnPostAuthToolkit) => OnPostAuthResult | Promise; +export declare type OnPostAuthHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPostAuthToolkit) => OnPostAuthResult | KibanaResponse | Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md index b9d7a1463a20..001c14c53fec 100644 --- a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md +++ b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md @@ -17,6 +17,4 @@ export interface OnPostAuthToolkit | Property | Type | Description | | --- | --- | --- | | [next](./kibana-plugin-server.onpostauthtoolkit.next.md) | () => OnPostAuthResult | To pass request to the next handler | -| [redirected](./kibana-plugin-server.onpostauthtoolkit.redirected.md) | (url: string) => OnPostAuthResult | To interrupt request handling and redirect to a configured url | -| [rejected](./kibana-plugin-server.onpostauthtoolkit.rejected.md) | (error: Error, options?: {
statusCode?: number;
}) => OnPostAuthResult | Fail the request with specified error. | diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.redirected.md deleted file mode 100644 index 94eab27724c8..000000000000 --- a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.redirected.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [redirected](./kibana-plugin-server.onpostauthtoolkit.redirected.md) - -## OnPostAuthToolkit.redirected property - -To interrupt request handling and redirect to a configured url - -Signature: - -```typescript -redirected: (url: string) => OnPostAuthResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.rejected.md deleted file mode 100644 index 00efb4fde305..000000000000 --- a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.rejected.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [rejected](./kibana-plugin-server.onpostauthtoolkit.rejected.md) - -## OnPostAuthToolkit.rejected property - -Fail the request with specified error. - -Signature: - -```typescript -rejected: (error: Error, options?: { - statusCode?: number; - }) => OnPostAuthResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md b/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md index 8374f83fc810..5eca904227bb 100644 --- a/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md +++ b/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type OnPreAuthHandler = (request: KibanaRequest, t: OnPreAuthToolkit) => OnPreAuthResult | Promise; +export declare type OnPreAuthHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPreAuthToolkit) => OnPreAuthResult | KibanaResponse | Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md index 787c9010372e..174f377eec29 100644 --- a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md @@ -17,6 +17,5 @@ export interface OnPreAuthToolkit | Property | Type | Description | | --- | --- | --- | | [next](./kibana-plugin-server.onpreauthtoolkit.next.md) | () => OnPreAuthResult | To pass request to the next handler | -| [redirected](./kibana-plugin-server.onpreauthtoolkit.redirected.md) | (url: string, options?: {
forward: boolean;
}) => OnPreAuthResult | To interrupt request handling and redirect to a configured url. If "options.forwarded" = true, request will be forwarded to another url right on the server. | -| [rejected](./kibana-plugin-server.onpreauthtoolkit.rejected.md) | (error: Error, options?: {
statusCode?: number;
}) => OnPreAuthResult | Fail the request with specified error. | +| [rewriteUrl](./kibana-plugin-server.onpreauthtoolkit.rewriteurl.md) | (url: string) => OnPreAuthResult | Rewrite requested resources url before is was authenticated and routed to a handler | diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.redirected.md deleted file mode 100644 index 77deb5b61c4e..000000000000 --- a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.redirected.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [redirected](./kibana-plugin-server.onpreauthtoolkit.redirected.md) - -## OnPreAuthToolkit.redirected property - -To interrupt request handling and redirect to a configured url. If "options.forwarded" = true, request will be forwarded to another url right on the server. - -Signature: - -```typescript -redirected: (url: string, options?: { - forward: boolean; - }) => OnPreAuthResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rejected.md deleted file mode 100644 index 1fd79d0b5766..000000000000 --- a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rejected.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [rejected](./kibana-plugin-server.onpreauthtoolkit.rejected.md) - -## OnPreAuthToolkit.rejected property - -Fail the request with specified error. - -Signature: - -```typescript -rejected: (error: Error, options?: { - statusCode?: number; - }) => OnPreAuthResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rewriteurl.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rewriteurl.md new file mode 100644 index 000000000000..0f401379c20f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rewriteurl.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [rewriteUrl](./kibana-plugin-server.onpreauthtoolkit.rewriteurl.md) + +## OnPreAuthToolkit.rewriteUrl property + +Rewrite requested resources url before is was authenticated and routed to a handler + +Signature: + +```typescript +rewriteUrl: (url: string) => OnPreAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md index 8eab84e2531d..2bba3d408f68 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md @@ -19,4 +19,5 @@ export interface PluginInitializerContext | [config](./kibana-plugin-server.plugininitializercontext.config.md) | {
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
} | | | [env](./kibana-plugin-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
} | | | [logger](./kibana-plugin-server.plugininitializercontext.logger.md) | LoggerFactory | | +| [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId | | diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.opaqueid.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.opaqueid.md new file mode 100644 index 000000000000..7ac177f039c9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.opaqueid.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) > [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md) + +## PluginInitializerContext.opaqueId property + +Signature: + +```typescript +opaqueId: PluginOpaqueId; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md new file mode 100644 index 000000000000..39c1eeda47e0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginManifest](./kibana-plugin-server.pluginmanifest.md) > [configPath](./kibana-plugin-server.pluginmanifest.configpath.md) + +## PluginManifest.configPath property + +Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id". + +Signature: + +```typescript +readonly configPath: ConfigPath; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md new file mode 100644 index 000000000000..44e61f11fa21 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginManifest](./kibana-plugin-server.pluginmanifest.md) > [id](./kibana-plugin-server.pluginmanifest.id.md) + +## PluginManifest.id property + +Identifier of the plugin. + +Signature: + +```typescript +readonly id: PluginName; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.kibanaversion.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.kibanaversion.md new file mode 100644 index 000000000000..f568dce9a8a9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.kibanaversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginManifest](./kibana-plugin-server.pluginmanifest.md) > [kibanaVersion](./kibana-plugin-server.pluginmanifest.kibanaversion.md) + +## PluginManifest.kibanaVersion property + +The version of Kibana the plugin is compatible with, defaults to "version". + +Signature: + +```typescript +readonly kibanaVersion: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.md new file mode 100644 index 000000000000..4a9498f0e9fa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.md @@ -0,0 +1,31 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginManifest](./kibana-plugin-server.pluginmanifest.md) + +## PluginManifest interface + +Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. + +Signature: + +```typescript +export interface PluginManifest +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [configPath](./kibana-plugin-server.pluginmanifest.configpath.md) | ConfigPath | Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id". | +| [id](./kibana-plugin-server.pluginmanifest.id.md) | PluginName | Identifier of the plugin. | +| [kibanaVersion](./kibana-plugin-server.pluginmanifest.kibanaversion.md) | string | The version of Kibana the plugin is compatible with, defaults to "version". | +| [optionalPlugins](./kibana-plugin-server.pluginmanifest.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | +| [requiredPlugins](./kibana-plugin-server.pluginmanifest.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | +| [server](./kibana-plugin-server.pluginmanifest.server.md) | boolean | Specifies whether plugin includes some server-side specific functionality. | +| [ui](./kibana-plugin-server.pluginmanifest.ui.md) | boolean | Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via public/ui_plugin.js file. | +| [version](./kibana-plugin-server.pluginmanifest.version.md) | string | Version of the plugin. | + +## Remarks + +Should never be used in code outside of Core but is exported for documentation purposes. + diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.optionalplugins.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.optionalplugins.md new file mode 100644 index 000000000000..692785a705d4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.optionalplugins.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginManifest](./kibana-plugin-server.pluginmanifest.md) > [optionalPlugins](./kibana-plugin-server.pluginmanifest.optionalplugins.md) + +## PluginManifest.optionalPlugins property + +An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. + +Signature: + +```typescript +readonly optionalPlugins: readonly PluginName[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.requiredplugins.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.requiredplugins.md new file mode 100644 index 000000000000..0ea7c872dfa0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.requiredplugins.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginManifest](./kibana-plugin-server.pluginmanifest.md) > [requiredPlugins](./kibana-plugin-server.pluginmanifest.requiredplugins.md) + +## PluginManifest.requiredPlugins property + +An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. + +Signature: + +```typescript +readonly requiredPlugins: readonly PluginName[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.server.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.server.md new file mode 100644 index 000000000000..676ad721edf7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.server.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginManifest](./kibana-plugin-server.pluginmanifest.md) > [server](./kibana-plugin-server.pluginmanifest.server.md) + +## PluginManifest.server property + +Specifies whether plugin includes some server-side specific functionality. + +Signature: + +```typescript +readonly server: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.ui.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.ui.md new file mode 100644 index 000000000000..ad5ce2237c58 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.ui.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginManifest](./kibana-plugin-server.pluginmanifest.md) > [ui](./kibana-plugin-server.pluginmanifest.ui.md) + +## PluginManifest.ui property + +Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via `public/ui_plugin.js` file. + +Signature: + +```typescript +readonly ui: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.version.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.version.md new file mode 100644 index 000000000000..75255096408f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.version.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginManifest](./kibana-plugin-server.pluginmanifest.md) > [version](./kibana-plugin-server.pluginmanifest.version.md) + +## PluginManifest.version property + +Version of the plugin. + +Signature: + +```typescript +readonly version: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginopaqueid.md b/docs/development/core/server/kibana-plugin-server.pluginopaqueid.md new file mode 100644 index 000000000000..3b2399d95137 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginopaqueid.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md) + +## PluginOpaqueId type + + +Signature: + +```typescript +export declare type PluginOpaqueId = symbol; +``` diff --git a/docs/development/core/server/kibana-plugin-server.redirectresponseoptions.md b/docs/development/core/server/kibana-plugin-server.redirectresponseoptions.md new file mode 100644 index 000000000000..6fb0a5add2fb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.redirectresponseoptions.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RedirectResponseOptions](./kibana-plugin-server.redirectresponseoptions.md) + +## RedirectResponseOptions type + +HTTP response parameters for redirection response + +Signature: + +```typescript +export declare type RedirectResponseOptions = HttpResponseOptions & { + headers: { + location: string; + }; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-server.requesthandler.md b/docs/development/core/server/kibana-plugin-server.requesthandler.md new file mode 100644 index 000000000000..b7e593c30f2f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.requesthandler.md @@ -0,0 +1,42 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RequestHandler](./kibana-plugin-server.requesthandler.md) + +## RequestHandler type + +A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) functions. + +Signature: + +```typescript +export declare type RequestHandler

= (request: KibanaRequest, TypeOf, TypeOf>, response: KibanaResponseFactory) => KibanaResponse | Promise>; +``` + +## Example + + +```ts +const router = new Router('my-app'); +// creates a route handler for GET request on 'my-app/path/{id}' path +router.get( + { + path: 'path/{id}', + // defines a validation schema for a named segment of the route path + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + // function to execute to create a responses + async (request, response) => { + const data = await findObject(request.params.id); + // creates a command to respond with 'not found' error + if (!data) return response.notFound(); + // creates a command to send found data to the client + return response.ok(data); + } +); + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.responseerror.md b/docs/development/core/server/kibana-plugin-server.responseerror.md new file mode 100644 index 000000000000..6aa4a4e97ff7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerror.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseError](./kibana-plugin-server.responseerror.md) + +## ResponseError type + +Error message and optional data send to the client in case of error. + +Signature: + +```typescript +export declare type ResponseError = string | Error | { + message: string | Error; + meta?: ResponseErrorMeta; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-server.responseerrormeta.data.md b/docs/development/core/server/kibana-plugin-server.responseerrormeta.data.md new file mode 100644 index 000000000000..afef0c88432a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerrormeta.data.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) > [data](./kibana-plugin-server.responseerrormeta.data.md) + +## ResponseErrorMeta.data property + +Signature: + +```typescript +data?: Record; +``` diff --git a/docs/development/core/server/kibana-plugin-server.responseerrormeta.doclink.md b/docs/development/core/server/kibana-plugin-server.responseerrormeta.doclink.md new file mode 100644 index 000000000000..472cb3ef48e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerrormeta.doclink.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) > [docLink](./kibana-plugin-server.responseerrormeta.doclink.md) + +## ResponseErrorMeta.docLink property + +Signature: + +```typescript +docLink?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.responseerrormeta.errorcode.md b/docs/development/core/server/kibana-plugin-server.responseerrormeta.errorcode.md new file mode 100644 index 000000000000..1f26f072e0b9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerrormeta.errorcode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) > [errorCode](./kibana-plugin-server.responseerrormeta.errorcode.md) + +## ResponseErrorMeta.errorCode property + +Signature: + +```typescript +errorCode?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.responseerrormeta.md b/docs/development/core/server/kibana-plugin-server.responseerrormeta.md new file mode 100644 index 000000000000..9ab351d013dd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.responseerrormeta.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ResponseErrorMeta](./kibana-plugin-server.responseerrormeta.md) + +## ResponseErrorMeta interface + +Additional metadata to enhance error output or provide error details. + +Signature: + +```typescript +export interface ResponseErrorMeta +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [data](./kibana-plugin-server.responseerrormeta.data.md) | Record<string, any> | | +| [docLink](./kibana-plugin-server.responseerrormeta.doclink.md) | string | | +| [errorCode](./kibana-plugin-server.responseerrormeta.errorcode.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.md b/docs/development/core/server/kibana-plugin-server.routeconfig.md new file mode 100644 index 000000000000..87ec365dc251 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) + +## RouteConfig interface + +Route specific configuration. + +Signature: + +```typescript +export interface RouteConfig

+``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [options](./kibana-plugin-server.routeconfig.options.md) | RouteConfigOptions | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md). | +| [path](./kibana-plugin-server.routeconfig.path.md) | string | The endpoint \_within\_ the router path to register the route. E.g. if the router is registered at /elasticsearch and the route path is /search, the full path for the route is /elasticsearch/search. Supports: - named path segments path/{name}. - optional path segments path/{position?}. - multi-segments path/{coordinates*2}. Segments are accessible within a handler function as params property of [KibanaRequest](./kibana-plugin-server.kibanarequest.md) object. To have read access to params you \*must\* specify validation schema with [RouteConfig.validate](./kibana-plugin-server.routeconfig.validate.md). | +| [validate](./kibana-plugin-server.routeconfig.validate.md) | RouteSchemas<P, Q, B> | false | A schema created with @kbn/config-schema that every request will be validated against. You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify false. | + diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.options.md b/docs/development/core/server/kibana-plugin-server.routeconfig.options.md new file mode 100644 index 000000000000..12ca36da6de7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.options.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [options](./kibana-plugin-server.routeconfig.options.md) + +## RouteConfig.options property + +Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md). + +Signature: + +```typescript +options?: RouteConfigOptions; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.path.md b/docs/development/core/server/kibana-plugin-server.routeconfig.path.md new file mode 100644 index 000000000000..3437f0e0fe06 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.path.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [path](./kibana-plugin-server.routeconfig.path.md) + +## RouteConfig.path property + +The endpoint \_within\_ the router path to register the route. E.g. if the router is registered at `/elasticsearch` and the route path is `/search`, the full path for the route is `/elasticsearch/search`. Supports: - named path segments `path/{name}`. - optional path segments `path/{position?}`. - multi-segments `path/{coordinates*2}`. Segments are accessible within a handler function as `params` property of [KibanaRequest](./kibana-plugin-server.kibanarequest.md) object. To have read access to `params` you \*must\* specify validation schema with [RouteConfig.validate](./kibana-plugin-server.routeconfig.validate.md). + +Signature: + +```typescript +path: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md new file mode 100644 index 000000000000..f7177485f5fb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [validate](./kibana-plugin-server.routeconfig.validate.md) + +## RouteConfig.validate property + +A schema created with `@kbn/config-schema` that every request will be validated against. You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `false`. + +Signature: + +```typescript +validate: RouteSchemas | false; +``` + +## Example + + +```ts + import { schema } from '@kbn/config-schema'; + router.get({ + path: 'path/{id}' + validate: { + params: schema.object({ + id: schema.string(), + }), + query: schema.object({...}), + body: schema.object({...}), + }, + }) + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md index 3fb4426c407c..2bb2491cae5d 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md @@ -4,7 +4,7 @@ ## RouteConfigOptions.authRequired property -A flag shows that authentication for a route: enabled when true disabled when false +A flag shows that authentication for a route: `enabled` when true `disabled` when false Enabled by default. diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md index 97e480c5490f..b4d210ac0b71 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md @@ -4,7 +4,7 @@ ## RouteConfigOptions interface -Route specific configuration. +Additional route options. Signature: @@ -16,6 +16,6 @@ export interface RouteConfigOptions | Property | Type | Description | | --- | --- | --- | -| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | +| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | | [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | diff --git a/docs/development/core/server/kibana-plugin-server.router.delete.md b/docs/development/core/server/kibana-plugin-server.router.delete.md index cd49f80baaf7..565dc10ce76e 100644 --- a/docs/development/core/server/kibana-plugin-server.router.delete.md +++ b/docs/development/core/server/kibana-plugin-server.router.delete.md @@ -4,7 +4,7 @@ ## Router.delete() method -Register a `DELETE` request with the router +Register a route handler for `DELETE` request. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.router.get.md b/docs/development/core/server/kibana-plugin-server.router.get.md index ab8e7c8c5a65..a3899eaa678f 100644 --- a/docs/development/core/server/kibana-plugin-server.router.get.md +++ b/docs/development/core/server/kibana-plugin-server.router.get.md @@ -4,7 +4,7 @@ ## Router.get() method -Register a `GET` request with the router +Register a route handler for `GET` request. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.router.getroutes.md b/docs/development/core/server/kibana-plugin-server.router.getroutes.md deleted file mode 100644 index 3e4785a3a7c6..000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.getroutes.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [getRoutes](./kibana-plugin-server.router.getroutes.md) - -## Router.getRoutes() method - -Returns all routes registered with the this router. - -Signature: - -```typescript -getRoutes(): Readonly[]; -``` -Returns: - -`Readonly[]` - -List of registered routes. - diff --git a/docs/development/core/server/kibana-plugin-server.router.md b/docs/development/core/server/kibana-plugin-server.router.md index 52193bbc553c..59a0a22ec7b5 100644 --- a/docs/development/core/server/kibana-plugin-server.router.md +++ b/docs/development/core/server/kibana-plugin-server.router.md @@ -4,6 +4,7 @@ ## Router class +Provides ability to declare a handler function for a particular path and HTTP request method. Each route can have only one handler functions, which is executed when the route is matched. Signature: @@ -22,15 +23,23 @@ export declare class Router | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [path](./kibana-plugin-server.router.path.md) | | string | | -| [routes](./kibana-plugin-server.router.routes.md) | | Array<Readonly<RouterRoute>> | | ## Methods | Method | Modifiers | Description | | --- | --- | --- | -| [delete(route, handler)](./kibana-plugin-server.router.delete.md) | | Register a DELETE request with the router | -| [get(route, handler)](./kibana-plugin-server.router.get.md) | | Register a GET request with the router | -| [getRoutes()](./kibana-plugin-server.router.getroutes.md) | | Returns all routes registered with the this router. | -| [post(route, handler)](./kibana-plugin-server.router.post.md) | | Register a POST request with the router | -| [put(route, handler)](./kibana-plugin-server.router.put.md) | | Register a PUT request with the router | +| [delete(route, handler)](./kibana-plugin-server.router.delete.md) | | Register a route handler for DELETE request. | +| [get(route, handler)](./kibana-plugin-server.router.get.md) | | Register a route handler for GET request. | +| [post(route, handler)](./kibana-plugin-server.router.post.md) | | Register a route handler for POST request. | +| [put(route, handler)](./kibana-plugin-server.router.put.md) | | Register a route handler for PUT request. | + +## Example + + +```ts +const router = new Router('my-app'); +// handler is called when 'my-app/path' resource is requested with `GET` method +router.get({ path: '/path', validate: false }, (req, res) => res.ok({ content: 'ok' })); + +``` diff --git a/docs/development/core/server/kibana-plugin-server.router.post.md b/docs/development/core/server/kibana-plugin-server.router.post.md index a499a46b1ee7..7aca35466d64 100644 --- a/docs/development/core/server/kibana-plugin-server.router.post.md +++ b/docs/development/core/server/kibana-plugin-server.router.post.md @@ -4,7 +4,7 @@ ## Router.post() method -Register a `POST` request with the router +Register a route handler for `POST` request. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.router.put.md b/docs/development/core/server/kibana-plugin-server.router.put.md index 7b1337279cca..760ccf9ef88e 100644 --- a/docs/development/core/server/kibana-plugin-server.router.put.md +++ b/docs/development/core/server/kibana-plugin-server.router.put.md @@ -4,7 +4,7 @@ ## Router.put() method -Register a `PUT` request with the router +Register a route handler for `PUT` request. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.router.routes.md b/docs/development/core/server/kibana-plugin-server.router.routes.md deleted file mode 100644 index c825bfe72d23..000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.routes.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [routes](./kibana-plugin-server.router.routes.md) - -## Router.routes property - -Signature: - -```typescript -routes: Array>; -``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md b/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md index b58408fe5f6b..c3d521aa7bc2 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md @@ -4,6 +4,8 @@ ## SavedObject.attributes property +The data for a Saved Object is stored in the `attributes` key as either an object or an array of objects. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.id.md b/docs/development/core/server/kibana-plugin-server.savedobject.id.md index f0ee1e9770f8..cc0127eb6ab0 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.id.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.id.md @@ -4,6 +4,8 @@ ## SavedObject.id property +The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.md b/docs/development/core/server/kibana-plugin-server.savedobject.md index 3b2417c56d5e..151498058874 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.md @@ -15,12 +15,12 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-server.savedobject.attributes.md) | T | | +| [attributes](./kibana-plugin-server.savedobject.attributes.md) | T | The data for a Saved Object is stored in the attributes key as either an object or an array of objects. | | [error](./kibana-plugin-server.savedobject.error.md) | {
message: string;
statusCode: number;
} | | -| [id](./kibana-plugin-server.savedobject.id.md) | string | | -| [migrationVersion](./kibana-plugin-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | | -| [references](./kibana-plugin-server.savedobject.references.md) | SavedObjectReference[] | | -| [type](./kibana-plugin-server.savedobject.type.md) | string | | -| [updated\_at](./kibana-plugin-server.savedobject.updated_at.md) | string | | -| [version](./kibana-plugin-server.savedobject.version.md) | string | | +| [id](./kibana-plugin-server.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | +| [migrationVersion](./kibana-plugin-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [references](./kibana-plugin-server.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | +| [type](./kibana-plugin-server.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | +| [updated\_at](./kibana-plugin-server.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | +| [version](./kibana-plugin-server.savedobject.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md index f9150a96b22c..63c353a8b2e7 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md @@ -4,6 +4,8 @@ ## SavedObject.migrationVersion property +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.references.md b/docs/development/core/server/kibana-plugin-server.savedobject.references.md index 08476527a446..22d4c84e9bca 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.references.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.references.md @@ -4,6 +4,8 @@ ## SavedObject.references property +A reference to another saved object. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.type.md b/docs/development/core/server/kibana-plugin-server.savedobject.type.md index 172de52d305f..0498b2d78048 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.type.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.type.md @@ -4,6 +4,8 @@ ## SavedObject.type property +The type of Saved Object. Each plugin can define it's own custom Saved Object types. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md b/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md index 0de1b1a0e816..bf57cbc08dbb 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md @@ -4,6 +4,8 @@ ## SavedObject.updated\_at property +Timestamp of the last time this document had been updated. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.version.md b/docs/development/core/server/kibana-plugin-server.savedobject.version.md index 25fbd536fcc3..a1c2f2c6c3b4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.version.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.version.md @@ -4,6 +4,8 @@ ## SavedObject.version property +An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectattribute.md b/docs/development/core/server/kibana-plugin-server.savedobjectattribute.md new file mode 100644 index 000000000000..6a6c7c4d36bc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectattribute.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) + +## SavedObjectAttribute type + + +Signature: + +```typescript +export declare type SavedObjectAttribute = string | number | boolean | null | undefined | SavedObjectAttributes | SavedObjectAttributes[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md b/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md index b629d8fad0c7..7ff1247e89f2 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md @@ -4,6 +4,7 @@ ## SavedObjectAttributes interface +The data for a Saved Object is stored in the `attributes` key as either an object or an array of objects. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md index 056de4b634b5..bae275777310 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md @@ -17,7 +17,7 @@ export interface SavedObjectsBulkCreateObjectT | | | [id](./kibana-plugin-server.savedobjectsbulkcreateobject.id.md) | string | | -| [migrationVersion](./kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | | +| [migrationVersion](./kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [references](./kibana-plugin-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[] | | | [type](./kibana-plugin-server.savedobjectsbulkcreateobject.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md index 9b33ab9a1b07..6065988a8d0f 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md @@ -4,6 +4,8 @@ ## SavedObjectsBulkCreateObject.migrationVersion property +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md index 3603904c2f89..ae948b090146 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md @@ -4,6 +4,8 @@ ## SavedObjectsClientContract type +Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. + \#\# SavedObjectsClient errors Since the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientprovideroptions.excludedwrappers.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientprovideroptions.excludedwrappers.md new file mode 100644 index 000000000000..9eb036c01e26 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientprovideroptions.excludedwrappers.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClientProviderOptions](./kibana-plugin-server.savedobjectsclientprovideroptions.md) > [excludedWrappers](./kibana-plugin-server.savedobjectsclientprovideroptions.excludedwrappers.md) + +## SavedObjectsClientProviderOptions.excludedWrappers property + +Signature: + +```typescript +excludedWrappers?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientprovideroptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientprovideroptions.md new file mode 100644 index 000000000000..29b872a30a37 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientprovideroptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClientProviderOptions](./kibana-plugin-server.savedobjectsclientprovideroptions.md) + +## SavedObjectsClientProviderOptions interface + +Options to control the creation of the Saved Objects Client. + +Signature: + +```typescript +export interface SavedObjectsClientProviderOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [excludedWrappers](./kibana-plugin-server.savedobjectsclientprovideroptions.excludedwrappers.md) | string[] | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperfactory.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperfactory.md index 321aefcba0ff..3ef2fac727b0 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperfactory.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperfactory.md @@ -4,6 +4,8 @@ ## SavedObjectsClientWrapperFactory type +Describes the factory used to create instances of Saved Objects Client Wrappers. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.md index 1a096fd9e526..65e7cfa64c2a 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.md @@ -4,6 +4,8 @@ ## SavedObjectsClientWrapperOptions interface +Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md index 61d65bfbf7b9..16549e420ac0 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md @@ -16,7 +16,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | | [id](./kibana-plugin-server.savedobjectscreateoptions.id.md) | string | (not recommended) Specify an id for the document | -| [migrationVersion](./kibana-plugin-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | | +| [migrationVersion](./kibana-plugin-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [overwrite](./kibana-plugin-server.savedobjectscreateoptions.overwrite.md) | boolean | Overwrite existing documents (defaults to false) | | [references](./kibana-plugin-server.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md index fcbec639312e..33432b1138d9 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md @@ -4,6 +4,8 @@ ## SavedObjectsCreateOptions.migrationVersion property +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.exportsizelimit.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.exportsizelimit.md new file mode 100644 index 000000000000..d8ff7b4c9e2e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.exportsizelimit.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) > [exportSizeLimit](./kibana-plugin-server.savedobjectsexportoptions.exportsizelimit.md) + +## SavedObjectsExportOptions.exportSizeLimit property + +Signature: + +```typescript +exportSizeLimit: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md new file mode 100644 index 000000000000..1972cc6634b7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) > [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) + +## SavedObjectsExportOptions.includeReferencesDeep property + +Signature: + +```typescript +includeReferencesDeep?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md new file mode 100644 index 000000000000..66f501a0f143 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) + +## SavedObjectsExportOptions interface + +Options controlling the export operation. + +Signature: + +```typescript +export interface SavedObjectsExportOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [exportSizeLimit](./kibana-plugin-server.savedobjectsexportoptions.exportsizelimit.md) | number | | +| [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) | boolean | | +| [namespace](./kibana-plugin-server.savedobjectsexportoptions.namespace.md) | string | | +| [objects](./kibana-plugin-server.savedobjectsexportoptions.objects.md) | Array<{
id: string;
type: string;
}> | | +| [savedObjectsClient](./kibana-plugin-server.savedobjectsexportoptions.savedobjectsclient.md) | SavedObjectsClientContract | | +| [types](./kibana-plugin-server.savedobjectsexportoptions.types.md) | string[] | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.namespace.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.namespace.md new file mode 100644 index 000000000000..b5abfba7f691 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.namespace.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) > [namespace](./kibana-plugin-server.savedobjectsexportoptions.namespace.md) + +## SavedObjectsExportOptions.namespace property + +Signature: + +```typescript +namespace?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.objects.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.objects.md new file mode 100644 index 000000000000..46cb62841d46 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.objects.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) > [objects](./kibana-plugin-server.savedobjectsexportoptions.objects.md) + +## SavedObjectsExportOptions.objects property + +Signature: + +```typescript +objects?: Array<{ + id: string; + type: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.savedobjectsclient.md new file mode 100644 index 000000000000..fc206d0f7e87 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.savedobjectsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) > [savedObjectsClient](./kibana-plugin-server.savedobjectsexportoptions.savedobjectsclient.md) + +## SavedObjectsExportOptions.savedObjectsClient property + +Signature: + +```typescript +savedObjectsClient: SavedObjectsClientContract; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.types.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.types.md new file mode 100644 index 000000000000..204402fe355e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.types.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) > [types](./kibana-plugin-server.savedobjectsexportoptions.types.md) + +## SavedObjectsExportOptions.types property + +Signature: + +```typescript +types?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md index 6d2cac4f1443..394363abb5d4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md @@ -4,8 +4,15 @@ ## SavedObjectsFindOptions.fields property +An array of fields to include in the results + Signature: ```typescript fields?: string[]; ``` + +## Example + +SavedObjects.find({type: 'dashboard', fields: \['attributes.name', 'attributes.location'\]}) + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md index 140b447c0002..ad81c439d902 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md @@ -16,12 +16,12 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | -| [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) | string[] | | +| [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | | [hasReference](./kibana-plugin-server.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [page](./kibana-plugin-server.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-server.savedobjectsfindoptions.perpage.md) | number | | -| [search](./kibana-plugin-server.savedobjectsfindoptions.search.md) | string | | -| [searchFields](./kibana-plugin-server.savedobjectsfindoptions.searchfields.md) | string[] | see Elasticsearch Simple Query String Query field argument for more information | +| [search](./kibana-plugin-server.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | +| [searchFields](./kibana-plugin-server.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-server.savedobjectsfindoptions.sortfield.md) | string | | | [sortOrder](./kibana-plugin-server.savedobjectsfindoptions.sortorder.md) | string | | | [type](./kibana-plugin-server.savedobjectsfindoptions.type.md) | string | string[] | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md index 7dca45e58123..a29dda189291 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md @@ -4,6 +4,8 @@ ## SavedObjectsFindOptions.search property +Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md index fdd157299c14..2505bac8aec4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md @@ -4,7 +4,7 @@ ## SavedObjectsFindOptions.searchFields property -see Elasticsearch Simple Query String Query field argument for more information +The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information Signature: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md index e4f7d1504298..23299e22d800 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md @@ -4,6 +4,9 @@ ## SavedObjectsFindResponse interface +Return type of the Saved Objects `find()` method. + +\*Note\*: this type is different between the Public and Server Saved Objects clients. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportconflicterror.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportconflicterror.md new file mode 100644 index 000000000000..485500da504a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportconflicterror.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportConflictError](./kibana-plugin-server.savedobjectsimportconflicterror.md) + +## SavedObjectsImportConflictError interface + +Represents a failure to import due to a conflict. + +Signature: + +```typescript +export interface SavedObjectsImportConflictError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./kibana-plugin-server.savedobjectsimportconflicterror.type.md) | 'conflict' | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportconflicterror.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportconflicterror.type.md new file mode 100644 index 000000000000..bd85de140674 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportconflicterror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportConflictError](./kibana-plugin-server.savedobjectsimportconflicterror.md) > [type](./kibana-plugin-server.savedobjectsimportconflicterror.type.md) + +## SavedObjectsImportConflictError.type property + +Signature: + +```typescript +type: 'conflict'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.error.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.error.md new file mode 100644 index 000000000000..0828ca9e01c3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportError](./kibana-plugin-server.savedobjectsimporterror.md) > [error](./kibana-plugin-server.savedobjectsimporterror.error.md) + +## SavedObjectsImportError.error property + +Signature: + +```typescript +error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.id.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.id.md new file mode 100644 index 000000000000..0791d668f462 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportError](./kibana-plugin-server.savedobjectsimporterror.md) > [id](./kibana-plugin-server.savedobjectsimporterror.id.md) + +## SavedObjectsImportError.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.md new file mode 100644 index 000000000000..0d734c21c315 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportError](./kibana-plugin-server.savedobjectsimporterror.md) + +## SavedObjectsImportError interface + +Represents a failure to import. + +Signature: + +```typescript +export interface SavedObjectsImportError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-server.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | +| [id](./kibana-plugin-server.savedobjectsimporterror.id.md) | string | | +| [title](./kibana-plugin-server.savedobjectsimporterror.title.md) | string | | +| [type](./kibana-plugin-server.savedobjectsimporterror.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.title.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.title.md new file mode 100644 index 000000000000..bd0beeb4d79d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportError](./kibana-plugin-server.savedobjectsimporterror.md) > [title](./kibana-plugin-server.savedobjectsimporterror.title.md) + +## SavedObjectsImportError.title property + +Signature: + +```typescript +title?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.type.md new file mode 100644 index 000000000000..0b48cc4bbaec --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimporterror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportError](./kibana-plugin-server.savedobjectsimporterror.md) > [type](./kibana-plugin-server.savedobjectsimporterror.type.md) + +## SavedObjectsImportError.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.blocking.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.blocking.md new file mode 100644 index 000000000000..bbbd499ea584 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.blocking.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportMissingReferencesError](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.md) > [blocking](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.blocking.md) + +## SavedObjectsImportMissingReferencesError.blocking property + +Signature: + +```typescript +blocking: Array<{ + type: string; + id: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.md new file mode 100644 index 000000000000..fb4e997fe17b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportMissingReferencesError](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.md) + +## SavedObjectsImportMissingReferencesError interface + +Represents a failure to import due to missing references. + +Signature: + +```typescript +export interface SavedObjectsImportMissingReferencesError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [blocking](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.blocking.md) | Array<{
type: string;
id: string;
}> | | +| [references](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.references.md) | Array<{
type: string;
id: string;
}> | | +| [type](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.type.md) | 'missing_references' | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.references.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.references.md new file mode 100644 index 000000000000..593d2b48d456 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.references.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportMissingReferencesError](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.md) > [references](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.references.md) + +## SavedObjectsImportMissingReferencesError.references property + +Signature: + +```typescript +references: Array<{ + type: string; + id: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.type.md new file mode 100644 index 000000000000..3e6e80f77c44 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportmissingreferenceserror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportMissingReferencesError](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.md) > [type](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.type.md) + +## SavedObjectsImportMissingReferencesError.type property + +Signature: + +```typescript +type: 'missing_references'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md new file mode 100644 index 000000000000..a1ea33e11b14 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportOptions](./kibana-plugin-server.savedobjectsimportoptions.md) + +## SavedObjectsImportOptions interface + +Options to control the import operation. + +Signature: + +```typescript +export interface SavedObjectsImportOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [namespace](./kibana-plugin-server.savedobjectsimportoptions.namespace.md) | string | | +| [objectLimit](./kibana-plugin-server.savedobjectsimportoptions.objectlimit.md) | number | | +| [overwrite](./kibana-plugin-server.savedobjectsimportoptions.overwrite.md) | boolean | | +| [readStream](./kibana-plugin-server.savedobjectsimportoptions.readstream.md) | Readable | | +| [savedObjectsClient](./kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md) | SavedObjectsClientContract | | +| [supportedTypes](./kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md) | string[] | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md new file mode 100644 index 000000000000..600a4dc1176f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.namespace.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportOptions](./kibana-plugin-server.savedobjectsimportoptions.md) > [namespace](./kibana-plugin-server.savedobjectsimportoptions.namespace.md) + +## SavedObjectsImportOptions.namespace property + +Signature: + +```typescript +namespace?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md new file mode 100644 index 000000000000..bb040a560b89 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.objectlimit.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportOptions](./kibana-plugin-server.savedobjectsimportoptions.md) > [objectLimit](./kibana-plugin-server.savedobjectsimportoptions.objectlimit.md) + +## SavedObjectsImportOptions.objectLimit property + +Signature: + +```typescript +objectLimit: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md new file mode 100644 index 000000000000..4586a9356858 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.overwrite.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportOptions](./kibana-plugin-server.savedobjectsimportoptions.md) > [overwrite](./kibana-plugin-server.savedobjectsimportoptions.overwrite.md) + +## SavedObjectsImportOptions.overwrite property + +Signature: + +```typescript +overwrite: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md new file mode 100644 index 000000000000..4b54f931797c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.readstream.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportOptions](./kibana-plugin-server.savedobjectsimportoptions.md) > [readStream](./kibana-plugin-server.savedobjectsimportoptions.readstream.md) + +## SavedObjectsImportOptions.readStream property + +Signature: + +```typescript +readStream: Readable; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md new file mode 100644 index 000000000000..f0d439aedc00 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportOptions](./kibana-plugin-server.savedobjectsimportoptions.md) > [savedObjectsClient](./kibana-plugin-server.savedobjectsimportoptions.savedobjectsclient.md) + +## SavedObjectsImportOptions.savedObjectsClient property + +Signature: + +```typescript +savedObjectsClient: SavedObjectsClientContract; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md new file mode 100644 index 000000000000..0359c53d8fcf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportOptions](./kibana-plugin-server.savedobjectsimportoptions.md) > [supportedTypes](./kibana-plugin-server.savedobjectsimportoptions.supportedtypes.md) + +## SavedObjectsImportOptions.supportedTypes property + +Signature: + +```typescript +supportedTypes: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.errors.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.errors.md new file mode 100644 index 000000000000..c59390c6d45e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.errors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportResponse](./kibana-plugin-server.savedobjectsimportresponse.md) > [errors](./kibana-plugin-server.savedobjectsimportresponse.errors.md) + +## SavedObjectsImportResponse.errors property + +Signature: + +```typescript +errors?: SavedObjectsImportError[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.md new file mode 100644 index 000000000000..23f6526dc636 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportResponse](./kibana-plugin-server.savedobjectsimportresponse.md) + +## SavedObjectsImportResponse interface + +The response describing the result of an import. + +Signature: + +```typescript +export interface SavedObjectsImportResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [errors](./kibana-plugin-server.savedobjectsimportresponse.errors.md) | SavedObjectsImportError[] | | +| [success](./kibana-plugin-server.savedobjectsimportresponse.success.md) | boolean | | +| [successCount](./kibana-plugin-server.savedobjectsimportresponse.successcount.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.success.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.success.md new file mode 100644 index 000000000000..5fd76959c556 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.success.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportResponse](./kibana-plugin-server.savedobjectsimportresponse.md) > [success](./kibana-plugin-server.savedobjectsimportresponse.success.md) + +## SavedObjectsImportResponse.success property + +Signature: + +```typescript +success: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.successcount.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.successcount.md new file mode 100644 index 000000000000..4b49f57e8367 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportresponse.successcount.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportResponse](./kibana-plugin-server.savedobjectsimportresponse.md) > [successCount](./kibana-plugin-server.savedobjectsimportresponse.successcount.md) + +## SavedObjectsImportResponse.successCount property + +Signature: + +```typescript +successCount: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.id.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.id.md new file mode 100644 index 000000000000..568185b2438b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportRetry](./kibana-plugin-server.savedobjectsimportretry.md) > [id](./kibana-plugin-server.savedobjectsimportretry.id.md) + +## SavedObjectsImportRetry.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.md new file mode 100644 index 000000000000..dc842afbf9f2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportRetry](./kibana-plugin-server.savedobjectsimportretry.md) + +## SavedObjectsImportRetry interface + +Describes a retry operation for importing a saved object. + +Signature: + +```typescript +export interface SavedObjectsImportRetry +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-server.savedobjectsimportretry.id.md) | string | | +| [overwrite](./kibana-plugin-server.savedobjectsimportretry.overwrite.md) | boolean | | +| [replaceReferences](./kibana-plugin-server.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | +| [type](./kibana-plugin-server.savedobjectsimportretry.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.overwrite.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.overwrite.md new file mode 100644 index 000000000000..36a31e836aeb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.overwrite.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportRetry](./kibana-plugin-server.savedobjectsimportretry.md) > [overwrite](./kibana-plugin-server.savedobjectsimportretry.overwrite.md) + +## SavedObjectsImportRetry.overwrite property + +Signature: + +```typescript +overwrite: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.replacereferences.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.replacereferences.md new file mode 100644 index 000000000000..c3439bb554e1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.replacereferences.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportRetry](./kibana-plugin-server.savedobjectsimportretry.md) > [replaceReferences](./kibana-plugin-server.savedobjectsimportretry.replacereferences.md) + +## SavedObjectsImportRetry.replaceReferences property + +Signature: + +```typescript +replaceReferences: Array<{ + type: string; + from: string; + to: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.type.md new file mode 100644 index 000000000000..8f0408dcbc11 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportretry.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportRetry](./kibana-plugin-server.savedobjectsimportretry.md) > [type](./kibana-plugin-server.savedobjectsimportretry.type.md) + +## SavedObjectsImportRetry.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.md new file mode 100644 index 000000000000..913038c5bc67 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportUnknownError](./kibana-plugin-server.savedobjectsimportunknownerror.md) + +## SavedObjectsImportUnknownError interface + +Represents a failure to import due to an unknown reason. + +Signature: + +```typescript +export interface SavedObjectsImportUnknownError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [message](./kibana-plugin-server.savedobjectsimportunknownerror.message.md) | string | | +| [statusCode](./kibana-plugin-server.savedobjectsimportunknownerror.statuscode.md) | number | | +| [type](./kibana-plugin-server.savedobjectsimportunknownerror.type.md) | 'unknown' | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.message.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.message.md new file mode 100644 index 000000000000..96b8b98bf6a9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.message.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportUnknownError](./kibana-plugin-server.savedobjectsimportunknownerror.md) > [message](./kibana-plugin-server.savedobjectsimportunknownerror.message.md) + +## SavedObjectsImportUnknownError.message property + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.statuscode.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.statuscode.md new file mode 100644 index 000000000000..9cdef84ff4ea --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.statuscode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportUnknownError](./kibana-plugin-server.savedobjectsimportunknownerror.md) > [statusCode](./kibana-plugin-server.savedobjectsimportunknownerror.statuscode.md) + +## SavedObjectsImportUnknownError.statusCode property + +Signature: + +```typescript +statusCode: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.type.md new file mode 100644 index 000000000000..cf31166157ab --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunknownerror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportUnknownError](./kibana-plugin-server.savedobjectsimportunknownerror.md) > [type](./kibana-plugin-server.savedobjectsimportunknownerror.type.md) + +## SavedObjectsImportUnknownError.type property + +Signature: + +```typescript +type: 'unknown'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportunsupportedtypeerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunsupportedtypeerror.md new file mode 100644 index 000000000000..cc775b20bb8f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunsupportedtypeerror.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-server.savedobjectsimportunsupportedtypeerror.md) + +## SavedObjectsImportUnsupportedTypeError interface + +Represents a failure to import due to having an unsupported saved object type. + +Signature: + +```typescript +export interface SavedObjectsImportUnsupportedTypeError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./kibana-plugin-server.savedobjectsimportunsupportedtypeerror.type.md) | 'unsupported_type' | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsimportunsupportedtypeerror.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunsupportedtypeerror.type.md new file mode 100644 index 000000000000..ae69911020c1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsimportunsupportedtypeerror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-server.savedobjectsimportunsupportedtypeerror.md) > [type](./kibana-plugin-server.savedobjectsimportunsupportedtypeerror.type.md) + +## SavedObjectsImportUnsupportedTypeError.type property + +Signature: + +```typescript +type: 'unsupported_type'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.debug.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.debug.md new file mode 100644 index 000000000000..be44bc7422d6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.debug.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) > [debug](./kibana-plugin-server.savedobjectsmigrationlogger.debug.md) + +## SavedObjectsMigrationLogger.debug property + +Signature: + +```typescript +debug: (msg: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.info.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.info.md new file mode 100644 index 000000000000..f8bbd5e4e625 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.info.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) > [info](./kibana-plugin-server.savedobjectsmigrationlogger.info.md) + +## SavedObjectsMigrationLogger.info property + +Signature: + +```typescript +info: (msg: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md new file mode 100644 index 000000000000..9e21cb0641ba --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) + +## SavedObjectsMigrationLogger interface + + +Signature: + +```typescript +export interface SavedObjectsMigrationLogger +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [debug](./kibana-plugin-server.savedobjectsmigrationlogger.debug.md) | (msg: string) => void | | +| [info](./kibana-plugin-server.savedobjectsmigrationlogger.info.md) | (msg: string) => void | | +| [warning](./kibana-plugin-server.savedobjectsmigrationlogger.warning.md) | (msg: string) => void | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md new file mode 100644 index 000000000000..978090f9fc88 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) > [warning](./kibana-plugin-server.savedobjectsmigrationlogger.warning.md) + +## SavedObjectsMigrationLogger.warning property + +Signature: + +```typescript +warning: (msg: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md index 434e46041cf7..b7f9c8fd8fe9 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md @@ -4,10 +4,15 @@ ## SavedObjectsMigrationVersion interface -A dictionary of saved object type -> version used to determine what migrations need to be applied to a saved object. +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. Signature: ```typescript export interface SavedObjectsMigrationVersion ``` + +## Example + +migrationVersion: { dashboard: '7.1.1', space: '6.6.6', } + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._id.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._id.md new file mode 100644 index 000000000000..cd16eadf5193 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) > [\_id](./kibana-plugin-server.savedobjectsrawdoc._id.md) + +## SavedObjectsRawDoc.\_id property + +Signature: + +```typescript +_id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._primary_term.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._primary_term.md new file mode 100644 index 000000000000..c5eef82322f5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._primary_term.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) > [\_primary\_term](./kibana-plugin-server.savedobjectsrawdoc._primary_term.md) + +## SavedObjectsRawDoc.\_primary\_term property + +Signature: + +```typescript +_primary_term?: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._seq_no.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._seq_no.md new file mode 100644 index 000000000000..a3b9a943a708 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._seq_no.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) > [\_seq\_no](./kibana-plugin-server.savedobjectsrawdoc._seq_no.md) + +## SavedObjectsRawDoc.\_seq\_no property + +Signature: + +```typescript +_seq_no?: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._source.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._source.md new file mode 100644 index 000000000000..1babaab14f14 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._source.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) > [\_source](./kibana-plugin-server.savedobjectsrawdoc._source.md) + +## SavedObjectsRawDoc.\_source property + +Signature: + +```typescript +_source: any; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._type.md new file mode 100644 index 000000000000..31c40e15b53c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) > [\_type](./kibana-plugin-server.savedobjectsrawdoc._type.md) + +## SavedObjectsRawDoc.\_type property + +Signature: + +```typescript +_type?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc.md new file mode 100644 index 000000000000..5864a8546539 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) + +## SavedObjectsRawDoc interface + +A raw document as represented directly in the saved object index. + +Signature: + +```typescript +export interface RawDoc +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [\_id](./kibana-plugin-server.savedobjectsrawdoc._id.md) | string | | +| [\_primary\_term](./kibana-plugin-server.savedobjectsrawdoc._primary_term.md) | number | | +| [\_seq\_no](./kibana-plugin-server.savedobjectsrawdoc._seq_no.md) | number | | +| [\_source](./kibana-plugin-server.savedobjectsrawdoc._source.md) | any | | +| [\_type](./kibana-plugin-server.savedobjectsrawdoc._type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md new file mode 100644 index 000000000000..e3542714d96b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) + +## SavedObjectsResolveImportErrorsOptions interface + +Options to control the "resolve import" operation. + +Signature: + +```typescript +export interface SavedObjectsResolveImportErrorsOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [namespace](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md) | string | | +| [objectLimit](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) | number | | +| [readStream](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md) | Readable | | +| [retries](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md) | SavedObjectsImportRetry[] | | +| [savedObjectsClient](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) | SavedObjectsClientContract | | +| [supportedTypes](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md) | string[] | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md new file mode 100644 index 000000000000..6175e75a4bbe --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) > [namespace](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.namespace.md) + +## SavedObjectsResolveImportErrorsOptions.namespace property + +Signature: + +```typescript +namespace?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md new file mode 100644 index 000000000000..d5616851e138 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) > [objectLimit](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.objectlimit.md) + +## SavedObjectsResolveImportErrorsOptions.objectLimit property + +Signature: + +```typescript +objectLimit: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md new file mode 100644 index 000000000000..e4b5d92d7b05 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) > [readStream](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.readstream.md) + +## SavedObjectsResolveImportErrorsOptions.readStream property + +Signature: + +```typescript +readStream: Readable; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md new file mode 100644 index 000000000000..7dc825f762fe --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) > [retries](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.retries.md) + +## SavedObjectsResolveImportErrorsOptions.retries property + +Signature: + +```typescript +retries: SavedObjectsImportRetry[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md new file mode 100644 index 000000000000..ae5edc98d3a9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) > [savedObjectsClient](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.savedobjectsclient.md) + +## SavedObjectsResolveImportErrorsOptions.savedObjectsClient property + +Signature: + +```typescript +savedObjectsClient: SavedObjectsClientContract; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md new file mode 100644 index 000000000000..5a92a8d0a993 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) > [supportedTypes](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.supportedtypes.md) + +## SavedObjectsResolveImportErrorsOptions.supportedTypes property + +Signature: + +```typescript +supportedTypes: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.(constructor).md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.(constructor).md new file mode 100644 index 000000000000..abac3bc88fac --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.(constructor).md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [(constructor)](./kibana-plugin-server.savedobjectsschema.(constructor).md) + +## SavedObjectsSchema.(constructor) + +Constructs a new instance of the `SavedObjectsSchema` class + +Signature: + +```typescript +constructor(schemaDefinition?: SavedObjectsSchemaDefinition); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| schemaDefinition | SavedObjectsSchemaDefinition | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md new file mode 100644 index 000000000000..5baf07546355 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [getConvertToAliasScript](./kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md) + +## SavedObjectsSchema.getConvertToAliasScript() method + +Signature: + +```typescript +getConvertToAliasScript(type: string): string | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`string | undefined` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md new file mode 100644 index 000000000000..ba1c439c8c6b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [getIndexForType](./kibana-plugin-server.savedobjectsschema.getindexfortype.md) + +## SavedObjectsSchema.getIndexForType() method + +Signature: + +```typescript +getIndexForType(config: Config, type: string): string | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| config | Config | | +| type | string | | + +Returns: + +`string | undefined` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md new file mode 100644 index 000000000000..f67b12a4d14c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [isHiddenType](./kibana-plugin-server.savedobjectsschema.ishiddentype.md) + +## SavedObjectsSchema.isHiddenType() method + +Signature: + +```typescript +isHiddenType(type: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md new file mode 100644 index 000000000000..2ca0abd7e4aa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [isNamespaceAgnostic](./kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md) + +## SavedObjectsSchema.isNamespaceAgnostic() method + +Signature: + +```typescript +isNamespaceAgnostic(type: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md new file mode 100644 index 000000000000..11962007c4c3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) + +## SavedObjectsSchema class + +Signature: + +```typescript +export declare class SavedObjectsSchema +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(schemaDefinition)](./kibana-plugin-server.savedobjectsschema.(constructor).md) | | Constructs a new instance of the SavedObjectsSchema class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [getConvertToAliasScript(type)](./kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md) | | | +| [getIndexForType(config, type)](./kibana-plugin-server.savedobjectsschema.getindexfortype.md) | | | +| [isHiddenType(type)](./kibana-plugin-server.savedobjectsschema.ishiddentype.md) | | | +| [isNamespaceAgnostic(type)](./kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md) | | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.(constructor).md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.(constructor).md new file mode 100644 index 000000000000..6524ff3e17ca --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.(constructor).md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [(constructor)](./kibana-plugin-server.savedobjectsserializer.(constructor).md) + +## SavedObjectsSerializer.(constructor) + +Constructs a new instance of the `SavedObjectsSerializer` class + +Signature: + +```typescript +constructor(schema: SavedObjectsSchema); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| schema | SavedObjectsSchema | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md new file mode 100644 index 000000000000..4705f48a201a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [generateRawId](./kibana-plugin-server.savedobjectsserializer.generaterawid.md) + +## SavedObjectsSerializer.generateRawId() method + +Given a saved object type and id, generates the compound id that is stored in the raw document. + +Signature: + +```typescript +generateRawId(namespace: string | undefined, type: string, id?: string): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| namespace | string | undefined | | +| type | string | | +| id | string | | + +Returns: + +`string` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md new file mode 100644 index 000000000000..e190e7bce8c0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [isRawSavedObject](./kibana-plugin-server.savedobjectsserializer.israwsavedobject.md) + +## SavedObjectsSerializer.isRawSavedObject() method + +Determines whether or not the raw document can be converted to a saved object. + +Signature: + +```typescript +isRawSavedObject(rawDoc: RawDoc): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| rawDoc | RawDoc | | + +Returns: + +`any` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md new file mode 100644 index 000000000000..205e29cb0727 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) + +## SavedObjectsSerializer class + +Signature: + +```typescript +export declare class SavedObjectsSerializer +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(schema)](./kibana-plugin-server.savedobjectsserializer.(constructor).md) | | Constructs a new instance of the SavedObjectsSerializer class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [generateRawId(namespace, type, id)](./kibana-plugin-server.savedobjectsserializer.generaterawid.md) | | Given a saved object type and id, generates the compound id that is stored in the raw document. | +| [isRawSavedObject(rawDoc)](./kibana-plugin-server.savedobjectsserializer.israwsavedobject.md) | | Determines whether or not the raw document can be converted to a saved object. | +| [rawToSavedObject(doc)](./kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md) | | Converts a document from the format that is stored in elasticsearch to the saved object client format. | +| [savedObjectToRaw(savedObj)](./kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md) | | Converts a document from the saved object client format to the format that is stored in elasticsearch. | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md new file mode 100644 index 000000000000..b36cdb3be64d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [rawToSavedObject](./kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md) + +## SavedObjectsSerializer.rawToSavedObject() method + +Converts a document from the format that is stored in elasticsearch to the saved object client format. + +Signature: + +```typescript +rawToSavedObject(doc: RawDoc): SanitizedSavedObjectDoc; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| doc | RawDoc | | + +Returns: + +`SanitizedSavedObjectDoc` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md new file mode 100644 index 000000000000..4854a97a845b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [savedObjectToRaw](./kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md) + +## SavedObjectsSerializer.savedObjectToRaw() method + +Converts a document from the saved object client format to the format that is stored in elasticsearch. + +Signature: + +```typescript +savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): RawDoc; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| savedObj | SanitizedSavedObjectDoc | | + +Returns: + +`RawDoc` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.importexport.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.importexport.md new file mode 100644 index 000000000000..f9b4e46712f4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.importexport.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [importExport](./kibana-plugin-server.savedobjectsservice.importexport.md) + +## SavedObjectsService.importExport property + +Signature: + +```typescript +importExport: { + objectLimit: number; + importSavedObjects(options: SavedObjectsImportOptions): Promise; + resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise; + getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md index ad281002854b..d9e23e6f1592 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md @@ -17,7 +17,9 @@ export interface SavedObjectsService | --- | --- | --- | | [addScopedSavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md) | ScopedSavedObjectsClientProvider<Request>['addClientWrapperFactory'] | | | [getScopedSavedObjectsClient](./kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md) | ScopedSavedObjectsClientProvider<Request>['getClient'] | | +| [importExport](./kibana-plugin-server.savedobjectsservice.importexport.md) | {
objectLimit: number;
importSavedObjects(options: SavedObjectsImportOptions): Promise<SavedObjectsImportResponse>;
resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise<SavedObjectsImportResponse>;
getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise<Readable>;
} | | | [SavedObjectsClient](./kibana-plugin-server.savedobjectsservice.savedobjectsclient.md) | typeof SavedObjectsClient | | +| [schema](./kibana-plugin-server.savedobjectsservice.schema.md) | SavedObjectsSchema | | | [types](./kibana-plugin-server.savedobjectsservice.types.md) | string[] | | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.schema.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.schema.md new file mode 100644 index 000000000000..be5682e6f034 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.schema.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [schema](./kibana-plugin-server.savedobjectsservice.schema.md) + +## SavedObjectsService.schema property + +Signature: + +```typescript +schema: SavedObjectsSchema; +``` diff --git a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md index 94b49e43a113..0fea07320b2f 100644 --- a/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md +++ b/docs/development/core/server/kibana-plugin-server.scopedclusterclient.(constructor).md @@ -9,7 +9,7 @@ Constructs a new instance of the `ScopedClusterClient` class Signature: ```typescript -constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: Record | undefined); +constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: Headers | undefined); ``` ## Parameters @@ -18,5 +18,5 @@ constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: | --- | --- | --- | | internalAPICaller | APICaller | | | scopedAPICaller | APICaller | | -| headers | Record<string, string | string[] | undefined> | undefined | | +| headers | Headers | undefined | | diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md new file mode 100644 index 000000000000..167ab03d7567 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) > [encryptionKey](./kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md) + +## SessionStorageCookieOptions.encryptionKey property + +A key used to encrypt a cookie value. Should be at least 32 characters long. + +Signature: + +```typescript +encryptionKey: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.issecure.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.issecure.md new file mode 100644 index 000000000000..824fc9d136a3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.issecure.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) > [isSecure](./kibana-plugin-server.sessionstoragecookieoptions.issecure.md) + +## SessionStorageCookieOptions.isSecure property + +Flag indicating whether the cookie should be sent only via a secure connection. + +Signature: + +```typescript +isSecure: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md new file mode 100644 index 000000000000..de412818142f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) + +## SessionStorageCookieOptions interface + +Configuration used to create HTTP session storage based on top of cookie mechanism. + +Signature: + +```typescript +export interface SessionStorageCookieOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [encryptionKey](./kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md) | string | A key used to encrypt a cookie value. Should be at least 32 characters long. | +| [isSecure](./kibana-plugin-server.sessionstoragecookieoptions.issecure.md) | boolean | Flag indicating whether the cookie should be sent only via a secure connection. | +| [name](./kibana-plugin-server.sessionstoragecookieoptions.name.md) | string | Name of the session cookie. | +| [validate](./kibana-plugin-server.sessionstoragecookieoptions.validate.md) | (sessionValue: T) => boolean | Promise<boolean> | Function called to validate a cookie content. | + diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.name.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.name.md new file mode 100644 index 000000000000..e6bc7ea3fe00 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.name.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) > [name](./kibana-plugin-server.sessionstoragecookieoptions.name.md) + +## SessionStorageCookieOptions.name property + +Name of the session cookie. + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md new file mode 100644 index 000000000000..f3cbfc0d84e1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) > [validate](./kibana-plugin-server.sessionstoragecookieoptions.validate.md) + +## SessionStorageCookieOptions.validate property + +Function called to validate a cookie content. + +Signature: + +```typescript +validate: (sessionValue: T) => boolean | Promise; +``` diff --git a/docs/getting-started.asciidoc b/docs/getting-started.asciidoc index 80ccd4eadc23..c1b31a5153d8 100644 --- a/docs/getting-started.asciidoc +++ b/docs/getting-started.asciidoc @@ -4,46 +4,56 @@ [partintro] -- -Ready to get some hands-on experience with {kib}? There are two ways to start: +You’re new to Kibana and want to give it a try. {kib} has sample data sets and +tutorials to help you get started. -* <> -+ -Load the Flights sample data and dashboard with one click and start -interacting with {kib} visualizations in seconds. +[float] +=== Sample data -* <> -+ -Manually load a data set and build your own visualizations and dashboard. +You can use the <> to take {kib} for a test ride without having +to go through the process of loading data yourself. With one click, +you can install a sample data set and start interacting with +{kib} visualizations in seconds. You can access the sample data +from the {kib} home page. -Before you begin, make sure you've <> and established -a {kibana-ref}/connect-to-elasticsearch.html[connection to Elasticsearch]. -You might also be interested in the -https://www.elastic.co/webinars/getting-started-kibana[Getting Started with Kibana] -video tutorial. +[float] -If you are running our https://cloud.elastic.co[hosted Elasticsearch Service] -on Elastic Cloud, you can access Kibana with a single click. --- +=== Add data tutorials +{kib} has built-in *Add Data* tutorials to help you set up +data flows in the Elastic Stack. These tutorials are available +from the Kibana home page. In *Add Data to Kibana*, find the data type +you’re interested in, and click its button to view a list of available tutorials. -include::getting-started/add-sample-data.asciidoc[] +[float] +=== Hands-on experience -include::getting-started/tutorial-sample-data.asciidoc[] +The following tutorials walk you through searching, analyzing, +and visualizing data. -include::getting-started/tutorial-sample-filter.asciidoc[] +* <>. You'll +learn to filter and query data, edit visualizations, and interact with dashboards. -include::getting-started/tutorial-sample-query.asciidoc[] +* <>. You'll manually load a data set and build +your own visualizations and dashboard. -include::getting-started/tutorial-sample-discover.asciidoc[] +[float] +=== Before you begin -include::getting-started/tutorial-sample-edit.asciidoc[] +Make sure you've <> and established +a <>. -include::getting-started/tutorial-sample-inspect.asciidoc[] +If you are running our https://cloud.elastic.co[hosted Elasticsearch Service] +on Elastic Cloud, you can access Kibana with a single click. -include::getting-started/tutorial-sample-remove.asciidoc[] -include::getting-started/tutorial-full-experience.asciidoc[] +-- + +include::getting-started/add-sample-data.asciidoc[] -include::getting-started/tutorial-load-dataset.asciidoc[] +include::getting-started/tutorial-sample-data.asciidoc[] + +include::getting-started/tutorial-full-experience.asciidoc[] include::getting-started/tutorial-define-index.asciidoc[] @@ -53,6 +63,3 @@ include::getting-started/tutorial-visualizing.asciidoc[] include::getting-started/tutorial-dashboard.asciidoc[] -include::getting-started/tutorial-inspect.asciidoc[] - -include::getting-started/wrapping-up.asciidoc[] diff --git a/docs/getting-started/add-sample-data.asciidoc b/docs/getting-started/add-sample-data.asciidoc index 341410989b92..ab4343160188 100644 --- a/docs/getting-started/add-sample-data.asciidoc +++ b/docs/getting-started/add-sample-data.asciidoc @@ -1,32 +1,28 @@ [[add-sample-data]] -== Get up and running with sample data +== Add sample data {kib} has several sample data sets that you can use to explore {kib} before loading your own data. -Sample data sets install prepackaged visualizations, dashboards, -{kibana-ref}/canvas-getting-started.html[Canvas workpads], -and {kibana-ref}/maps.html[Maps]. - -The sample data sets showcase a variety of use cases: +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. -* *Flight data* enables you to view and interact with flight routes for four airlines. - -To get started, go to the home page and click the link next to *Add sample data*. - -Once you have loaded a data set, click *View data* to view visualizations in *Dashboard*. -*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. +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 +=== Next steps -Play with the sample flight data in the {kibana-ref}/tutorial-sample-data.html[flight dashboard tutorial]. +* Explore {kib} by following the <>. -Learn how to load data, define index patterns and build visualizations by {kibana-ref}/tutorial-build-dashboard.html[building your own dashboard]. +* Learn how to load data, define index patterns, and build visualizations by <>. diff --git a/docs/getting-started/tutorial-dashboard.asciidoc b/docs/getting-started/tutorial-dashboard.asciidoc index 5d1d923e6664..aab93eb51ca2 100644 --- a/docs/getting-started/tutorial-dashboard.asciidoc +++ b/docs/getting-started/tutorial-dashboard.asciidoc @@ -1,27 +1,57 @@ [[tutorial-dashboard]] -=== Displaying your visualizations in a dashboard +=== Add visualizations to a dashboard A dashboard is a collection of visualizations that you can arrange and share. You'll build a dashboard that contains the visualizations you saved during this tutorial. . Open *Dashboard*. -. Click *Create new dashboard*. -. Click *Add*. +. On the Dashboard overview page, click *Create new dashboard*. +. Click *Add* in the menu bar. . Add *Bar Example*, *Map Example*, *Markdown Example*, and *Pie Example*. - - -Your sample dashboard look like this: - ++ +Your sample dashboard should look 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. -To get a link to share or HTML code to embed the dashboard in a web page, save -the dashboard and click *Share*. +. *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. + +. 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*. ++ +[role="screenshot"] +image::images/tutorial-full-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] +=== Next steps + +Now that you have a handle on the basics, you're ready to start exploring +your own data with Kibana. + +* See {kibana-ref}/discover.html[Discover] for information about searching and filtering +your data. +* See {kibana-ref}/visualize.html[Visualize] for information about the visualization +types Kibana has to offer. +* See {kibana-ref}/management.html[Management] for information about configuring Kibana +and managing your saved objects. +* See {kibana-ref}/console-kibana.html[Console] to learn about the interactive +console you can use to submit REST requests to Elasticsearch. -*Save* your dashboard. diff --git a/docs/getting-started/tutorial-define-index.asciidoc b/docs/getting-started/tutorial-define-index.asciidoc index 032845058aac..f8ffb47ab8c0 100644 --- a/docs/getting-started/tutorial-define-index.asciidoc +++ b/docs/getting-started/tutorial-define-index.asciidoc @@ -1,45 +1,53 @@ [[tutorial-define-index]] -=== Defining your index patterns +=== Define your index patterns Index patterns tell Kibana which Elasticsearch indices you want to explore. An index pattern can match the name of a single index, or include a wildcard -(*) to match multiple indices. +(*) to match multiple indices. For example, Logstash typically creates a 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*`. -You'll create patterns for the Shakespeare data set, which has an + +[float] +==== Create your first index pattern + +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. +`bank`. These data sets don't contain time series data. . In Kibana, open *Management*, and then click *Index Patterns.* . If this is your first index pattern, the *Create index pattern* page opens automatically. -Otherwise, click *Create index pattern* in the upper left. +Otherwise, click *Create index pattern*. . Enter `shakes*` in the *Index pattern* field. + [role="screenshot"] image::images/tutorial-pattern-1.png[] . Click *Next step*. -. In *Configure settings*, click *Create index pattern*. For this pattern, -you don't need to configure any settings. -. Define a second index pattern named `ba*` You don't need to configure any settings for this pattern. +. In *Configure settings*, click *Create index pattern*. ++ +You’re presented a table of all fields and associated data types in the index. + +. Return to the *Index patterns* overview page and define a second index pattern named `ba*`. + +[float] +==== Create an index pattern for time series data -Now create an index pattern for the Logstash data set. This data set -contains time-series data. +Now create an index pattern for the Logstash index, which +contains time series data. . Define an index pattern named `logstash*`. . Click *Next step*. -. In *Configure settings*, select *@timestamp* in the *Time Filter field name* dropdown menu. +. Open the *Time Filter field name* dropdown and select *@timestamp*. . Click *Create index pattern*. - - - 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, go to *Dev Tools > Console* and enter `GET _cat/indices`. Alternately, use `curl -XGET "http://localhost:9200/_cat/indices"`. + + diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc index cddf09c2532a..48e5bed6a4ba 100644 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ b/docs/getting-started/tutorial-discovering.asciidoc @@ -1,5 +1,5 @@ [[tutorial-discovering]] -=== Discovering your data +=== Discover your data Using the Discover application, you can enter an {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch @@ -11,23 +11,26 @@ The current index pattern appears below the filter bar, in this case `shakes*`. You might need to click *New* in the menu bar to refresh the data. . Click the caret to the right of the current index pattern, and select `ba*`. ++ +By default, all fields are shown for each matching document. + . In the search field, enter the following string: + [source,text] account_number<100 AND balance>47500 - ++ The search returns all account numbers between zero and 99 with balances in -excess of 47,500. It returns results for account numbers 8, 32, 78, 85, and 97. - +excess of 47,500. Results appear for account numbers 8, 32, 78, 85, and 97. ++ [role="screenshot"] image::images/tutorial-discover-2.png[] - -By default, all fields are shown for each matching document. To choose which -fields to display, hover the pointer over the list of *Available Fields* ++ +. 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 `account_number` field, the display changes to a list of five account numbers. - ++ [role="screenshot"] image::images/tutorial-discover-3.png[] diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc index 2096e0191f7c..08f65b0a2409 100644 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ b/docs/getting-started/tutorial-full-experience.asciidoc @@ -1,12 +1,213 @@ [[tutorial-build-dashboard]] -== Building your own dashboard +== Build your own dashboard -Ready to load some data and build a dashboard? This tutorial shows you how to: +Want to load some data into Kibana and build a dashboard? This tutorial shows you how to: -* Load a data set into Elasticsearch -* Define an index pattern -* Discover and explore the data -* Visualize the data -* Add visualizations to a dashboard -* Inspect the data behind a visualization +* <> +* <> +* <> +* <> +* <> +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 + +This tutorial requires you to download three data sets: + +* The complete works of William Shakespeare, suitably parsed into fields +* A set of fictitious 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] +curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/shakespeare.json +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: + +[source,shell] +unzip accounts.zip +gunzip logs.jsonl.gz + +[float] +==== Structure of the data sets + +The Shakespeare data set has this structure: + +[source,json] +{ + "line_id": INT, + "play_name": "String", + "speech_number": INT, + "line_number": "String", + "speaker": "String", + "text_entry": "String", +} + +The accounts data set is structured as follows: + +[source,json] +{ + "account_number": INT, + "balance": INT, + "firstname": "String", + "lastname": "String", + "age": INT, + "gender": "M or F", + "address": "String", + "employer": "String", + "email": "String", + "city": "String", + "state": "String" +} + +The logs data set has dozens of different fields. Here are the notable fields for this tutorial: + +[source,json] +{ + "memory": INT, + "geo.coordinates": "geo_point" + "@timestamp": "date" +} + +[float] +==== Set up mappings + +Before you load the Shakespeare and logs data sets, you must set up {ref}/mapping.html[_mappings_] for the fields. +Mappings divide the documents in the index into logical groups and specify the characteristics +of the fields. These characteristics include the searchability of the field +and whether it's _tokenized_, or broken up into separate words. + +NOTE: If security is enabled, you must have the `all` Kibana privilege to run this tutorial. +You must also have the `create`, `manage` `read`, `write,` and `delete` +index privileges. See {xpack-ref}/security-privileges.html[Security Privileges] +for more information. + +In Kibana *Dev Tools > Console*, set up a mapping for the Shakespeare data set: + +[source,js] +PUT /shakespeare +{ + "mappings": { + "properties": { + "speaker": {"type": "keyword"}, + "play_name": {"type": "keyword"}, + "line_id": {"type": "integer"}, + "speech_number": {"type": "integer"} + } + } +} + +//CONSOLE + +This 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 +as geographic locations by applying the `geo_point` type. + +[source,js] +PUT /logstash-2015.05.18 +{ + "mappings": { + "properties": { + "geo": { + "properties": { + "coordinates": { + "type": "geo_point" + } + } + } + } + } +} + +//CONSOLE + +[source,js] +PUT /logstash-2015.05.19 +{ + "mappings": { + "properties": { + "geo": { + "properties": { + "coordinates": { + "type": "geo_point" + } + } + } + } + } +} + +//CONSOLE + +[source,js] +PUT /logstash-2015.05.20 +{ + "mappings": { + "properties": { + "geo": { + "properties": { + "coordinates": { + "type": "geo_point" + } + } + } + } + } +} + +//CONSOLE + +The accounts data set doesn't require any mappings. + +[float] +==== Load the data sets + +At this point, you're ready to use the Elasticsearch {ref}/docs-bulk.html[bulk] +API to load the data sets: + +[source,shell] +curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/bank/account/_bulk?pretty' --data-binary @accounts.json +curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/shakespeare/_bulk?pretty' --data-binary @shakespeare.json +curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/_bulk?pretty' --data-binary @logs.jsonl + +Or for Windows users, in Powershell: +[source,shell] +Invoke-RestMethod "http://:/bank/account/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "accounts.json" +Invoke-RestMethod "http://:/shakespeare/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "shakespeare.json" +Invoke-RestMethod "http://:/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "logs.jsonl" + +These commands might take some time to execute, depending on the available computing resources. + +Verify successful loading: + +[source,js] +GET /_cat/indices?v + +//CONSOLE + +Your output should look similar to this: + +[source,shell] +health status index pri rep docs.count docs.deleted store.size pri.store.size +yellow open bank 1 1 1000 0 418.2kb 418.2kb +yellow open shakespeare 1 1 111396 0 17.6mb 17.6mb +yellow open logstash-2015.05.18 1 1 4631 0 15.6mb 15.6mb +yellow open logstash-2015.05.19 1 1 4624 0 15.7mb 15.7mb +yellow open logstash-2015.05.20 1 1 4750 0 16.4mb 16.4mb diff --git a/docs/getting-started/tutorial-inspect.asciidoc b/docs/getting-started/tutorial-inspect.asciidoc deleted file mode 100644 index 7b028a3b6c51..000000000000 --- a/docs/getting-started/tutorial-inspect.asciidoc +++ /dev/null @@ -1,24 +0,0 @@ -[[tutorial-inspect]] -=== Inspecting 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. - -. In the dashboard, hover the pointer over the pie chart. -. Click the icon in the upper right. -. From the *Options* menu, select *Inspect*. -+ -[role="screenshot"] -image::images/tutorial-full-inspect1.png[] - -You can also look at the query used to fetch the data for the visualization. - -. Open the *View:Data* menu and select *Requests*. -. Click the tabs to look at the request statistics, the Elasticsearch request, -and the response in JSON. -. To close the Inspector, click X in the upper right. -+ -[role="screenshot"] -image::images/tutorial-full-inspect2.png[] diff --git a/docs/getting-started/tutorial-load-dataset.asciidoc b/docs/getting-started/tutorial-load-dataset.asciidoc deleted file mode 100644 index b0bcb876c718..000000000000 --- a/docs/getting-started/tutorial-load-dataset.asciidoc +++ /dev/null @@ -1,190 +0,0 @@ -[[tutorial-load-dataset]] -=== Loading sample data - -This tutorial requires three data sets: - -* The complete works of William Shakespeare, suitably parsed into fields -* A set of fictitious accounts with randomly generated data -* A set of randomly generated log files - -Create a new working directory where you want to download the files. From that directory, run the following commands: - -[source,shell] -curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/shakespeare.json -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: - -[source,shell] -unzip accounts.zip -gunzip logs.jsonl.gz - -==== Structure of the data sets - -The Shakespeare data set has this structure: - -[source,json] -{ - "line_id": INT, - "play_name": "String", - "speech_number": INT, - "line_number": "String", - "speaker": "String", - "text_entry": "String", -} - -The accounts data set is structured as follows: - -[source,json] -{ - "account_number": INT, - "balance": INT, - "firstname": "String", - "lastname": "String", - "age": INT, - "gender": "M or F", - "address": "String", - "employer": "String", - "email": "String", - "city": "String", - "state": "String" -} - -The logs data set has dozens of different fields. Here are the notable fields for this tutorial: - -[source,json] -{ - "memory": INT, - "geo.coordinates": "geo_point" - "@timestamp": "date" -} - -==== Set up mappings - -Before you load the Shakespeare and logs data sets, you must set up {ref}/mapping.html[_mappings_] for the fields. -Mappings divide the documents in the index into logical groups and specify the characteristics -of the fields. These characteristics include the searchability of the field -and whether it's _tokenized_, or broken up into separate words. - -NOTE: If security is enabled, you must have the `all` Kibana privilege to run this tutorial. -You must also have the `create`, `manage` `read`, `write,` and `delete` -index privileges. See {xpack-ref}/security-privileges.html[Security Privileges] -for more information. - -In Kibana *Dev Tools > Console*, set up a mapping for the Shakespeare data set: - -[source,js] -PUT /shakespeare -{ - "mappings": { - "properties": { - "speaker": {"type": "keyword"}, - "play_name": {"type": "keyword"}, - "line_id": {"type": "integer"}, - "speech_number": {"type": "integer"} - } - } -} - -//CONSOLE - -This 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 -as geographic locations by applying the `geo_point` type. - -[source,js] -PUT /logstash-2015.05.18 -{ - "mappings": { - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } - } -} - -//CONSOLE - -[source,js] -PUT /logstash-2015.05.19 -{ - "mappings": { - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } - } -} - -//CONSOLE - -[source,js] -PUT /logstash-2015.05.20 -{ - "mappings": { - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } - } -} - -//CONSOLE - -The accounts data set doesn't require any mappings. - -==== Load the data sets - -At this point, you're ready to use the Elasticsearch {ref}/docs-bulk.html[bulk] -API to load the data sets: - -[source,shell] -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/bank/account/_bulk?pretty' --data-binary @accounts.json -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/shakespeare/_bulk?pretty' --data-binary @shakespeare.json -curl -u elastic -H 'Content-Type: application/x-ndjson' -XPOST ':/_bulk?pretty' --data-binary @logs.jsonl - -Or for Windows users, in Powershell: -[source,shell] -Invoke-RestMethod "http://:/bank/account/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "accounts.json" -Invoke-RestMethod "http://:/shakespeare/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "shakespeare.json" -Invoke-RestMethod "http://:/_bulk?pretty" -Method Post -ContentType 'application/x-ndjson' -InFile "logs.jsonl" - -These commands might take some time to execute, depending on the available computing resources. - -Verify successful loading: - -[source,js] -GET /_cat/indices?v - -//CONSOLE - -Your output should look similar to this: - -[source,shell] -health status index pri rep docs.count docs.deleted store.size pri.store.size -yellow open bank 1 1 1000 0 418.2kb 418.2kb -yellow open shakespeare 1 1 111396 0 17.6mb 17.6mb -yellow open logstash-2015.05.18 1 1 4631 0 15.6mb 15.6mb -yellow open logstash-2015.05.19 1 1 4624 0 15.7mb 15.7mb -yellow open logstash-2015.05.20 1 1 4750 0 16.4mb 16.4mb diff --git a/docs/getting-started/tutorial-sample-data.asciidoc b/docs/getting-started/tutorial-sample-data.asciidoc index d064f41c6507..24cc176d5daf 100644 --- a/docs/getting-started/tutorial-sample-data.asciidoc +++ b/docs/getting-started/tutorial-sample-data.asciidoc @@ -1,31 +1,207 @@ [[tutorial-sample-data]] -== Explore {kib} using the Flight dashboard +== Explore {kib} using sample data -You’re new to {kib} and want to try it out. With one click, you can install -the Flights sample data and start interacting with Kibana. +Ready to get some hands-on experience with Kibana? +In this tutorial, you’ll work +with Kibana sample data and learn to: -The Flights data set contains data for four airlines. -You can load the data and preconfigured dashboard from the {kib} home page. +* <> +* <> +* <> +* <> -. On the home page, click the link next to *Sample data*. -. On the *Sample flight data* card, click *Add*. -. Click *View data*. +NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges +on the `kibana_sample_data_*` indices. See {xpack-ref}/security-privileges.html[Security Privileges] +for more information. + + +[float] +=== Add sample data + +Install the Flights sample data set, if you haven't already. + +. On the {kib} home page, click the link underneath *Add sample data*. +. 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::images/tutorial-sample-dashboard.png[] -In this tutorial, you’ll learn to: +[float] +[[tutorial-sample-filter]] +=== Filter and query 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]. + +[float] +==== Filter the data + +. In the *Controls* visualization, set an *Origin City* and a *Destination City*. +. Click *Apply changes*. ++ +The `OriginCityName` and the `DestCityName` fields are filtered to match +the data you specified. ++ +For example, this dashboard shows the data for flights from London to Oslo. ++ +[role="screenshot"] +image::images/tutorial-sample-filter.png[] + +. To add a filter manually, click *Add filter* in the filter bar, +and specify the data you want to view. + +. When you are finished experimenting, remove all filters. + + +[float] +[[tutorial-sample-query]] +==== Query the data + +. To find all flights out of Rome, enter this query in the query bar and click *Update*: ++ +[source,text] +OriginCityName:Rome + +. 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::images/tutorial-sample-query.png[] + +. When you are finished exploring the dashboard, remove the query by +clearing the contents in the query bar and clicking *Update*. + +[float] +[[tutorial-sample-discover]] +=== Discover the data + +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 side navigation, click *Discover*. + +. Ensure `kibana_sample_data_flights` is the current index pattern. +You might need to click *New* in the menu bar to refresh the data. ++ +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. ++ +[role="screenshot"] +image::images/tutorial-sample-discover1.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. ++ +[role="screenshot"] +image::images/tutorial-sample-discover2.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. + +[float] +==== Create a bucket aggregation + +. 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[]. ++ +You now see the average ticket price for all four airlines. ++ +[role="screenshot"] +image::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. ++ +[role="screenshot"] +image::images/tutorial-sample-edit2.png[] + +[float] +[[tutorial-sample-inspect]] +=== 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. + +. 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*. ++ +The initial view shows the document count. ++ +[role="screenshot"] +image::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. + +. Go to the *Sample data* page. +. On the *Sample flight data* card, click *Remove*. + +[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: + +* Load data +* Define an index pattern +* Discover and explore data +* Create visualizations +* Add visualizations to a dashboard + -* Filter the data -* Query the data -* Discover the data -* Edit a visualization -* Inspect the data behind the scenes -NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges -on the `kibana_sample_data_*` indices. See {xpack-ref}/security-privileges.html[Security Privileges] -for more information. diff --git a/docs/getting-started/tutorial-sample-discover.asciidoc b/docs/getting-started/tutorial-sample-discover.asciidoc deleted file mode 100644 index e455159f4d6c..000000000000 --- a/docs/getting-started/tutorial-sample-discover.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[[tutorial-sample-discover]] -=== Using Discover - -In the Discover application, the Flight data is presented in a table. You can -interactively explore the data, including searching and filtering. - -* In the side navigation, select *Discover*. - -The current index pattern appears below the filter bar. An -<> tells {kib} which {es} indices you want to -explore. - -The `kibana_sample_data_flights` index contains a time field. A histogram -shows the distribution of documents over time. - -[role="screenshot"] -image::images/tutorial-sample-discover1.png[] - -By default, all fields are shown for each matching document. To choose which fields to display, -hover the pointer over the 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: - -[role="screenshot"] -image::images/tutorial-sample-discover2.png[] diff --git a/docs/getting-started/tutorial-sample-edit.asciidoc b/docs/getting-started/tutorial-sample-edit.asciidoc deleted file mode 100644 index d009161716b3..000000000000 --- a/docs/getting-started/tutorial-sample-edit.asciidoc +++ /dev/null @@ -1,45 +0,0 @@ -[[tutorial-sample-edit]] -=== Editing 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. - -. Go to 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*. - -==== Edit a metric 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. - -. In the *Buckets* pane, select *Split Group*. -. In the *Aggregation* dropdown menu, select *Terms*. -. In the *Field* dropdown, select *Carrier*. -. Set *Descending* to four. -. Click *Apply changes* image:images/apply-changes-button.png[]. - -You now see the average ticket price for all four airlines. - -[role="screenshot"] -image::images/tutorial-sample-edit1.png[] - -==== Save the changes - -. In the menu bar, click *Save*. -. Leave the visualization name unchanged and click *Save*. -. Go to the *Global Flight* 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. -. In the menu bar, click *Save* and then confirm the save. -+ -[role="screenshot"] -image::images/tutorial-sample-edit2.png[] - diff --git a/docs/getting-started/tutorial-sample-filter.asciidoc b/docs/getting-started/tutorial-sample-filter.asciidoc deleted file mode 100644 index 3efca0e1d2b5..000000000000 --- a/docs/getting-started/tutorial-sample-filter.asciidoc +++ /dev/null @@ -1,23 +0,0 @@ -[[tutorial-sample-filter]] -=== Filtering the data - -Many visualizations in the *Global Flight* dashboard are interactive. You can -apply filters to modify the view of the data across all visualizations. - -. In the *Controls* visualization, set an *Origin City* and a *Destination City*. -. Click *Apply changes*. -+ -The `OriginCityName` and the `DestCityName` fields are filtered to match -the data you specified. -+ -For example, this dashboard shows the data for flights from London to Newark -and Pittsburgh. -+ -[role="screenshot"] -image::images/tutorial-sample-filter.png[] -+ -. To remove the filters, in the *Controls* visualization, click *Clear form*, and then -*Apply changes*. - -You can also add filters manually. In the filter bar, click *Add a Filter* -and specify the data you want to view. diff --git a/docs/getting-started/tutorial-sample-inspect.asciidoc b/docs/getting-started/tutorial-sample-inspect.asciidoc deleted file mode 100644 index 4ba74a3529a9..000000000000 --- a/docs/getting-started/tutorial-sample-inspect.asciidoc +++ /dev/null @@ -1,24 +0,0 @@ -[[tutorial-sample-inspect]] -=== Inspecting 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. - -. Hover the pointer over the *Flight Count and Average Ticket Price* visualization. -. Click the icon in the upper right. -. From the *Options* menu, select *Inspect*. -+ -[role="screenshot"] -image::images/tutorial-sample-inspect1.png[] - -You can also look at the query used to fetch the data for the visualization. - -. Open the *View: Data* menu and select *Requests*. -. Click the tabs to look at the request statistics, the Elasticsearch request, -and the response in JSON. -. To close the editor, click X in the upper right. -+ -[role="screenshot"] -image::images/tutorial-sample-inspect2.png[] \ No newline at end of file diff --git a/docs/getting-started/tutorial-sample-query.asciidoc b/docs/getting-started/tutorial-sample-query.asciidoc deleted file mode 100644 index 5a638bbe3a5c..000000000000 --- a/docs/getting-started/tutorial-sample-query.asciidoc +++ /dev/null @@ -1,30 +0,0 @@ -[[tutorial-sample-query]] -=== Querying the data - -You can enter an {es} query to narrow the view of the data. - -. To find all flights out of Rome, submit this query: -+ -[source,text] -OriginCityName:Rome - -. 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::images/tutorial-sample-query.png[] - -. When you are finished exploring the dashboard, remove the query by -clearing the contents in the query bar and pressing Enter. - -In general, filters are faster than queries. For more information, see {ref}/query-filter-context.html[Query and filter context]. - -TIP: {kib} has an experimental autocomplete feature that can -help jumpstart your queries. To turn on this feature, click *Options* on the -right of the query bar and opt in. With autocomplete enabled, -search suggestions are displayed when you start typing your query. \ No newline at end of file diff --git a/docs/getting-started/tutorial-sample-remove.asciidoc b/docs/getting-started/tutorial-sample-remove.asciidoc deleted file mode 100644 index 9761b3bdf198..000000000000 --- a/docs/getting-started/tutorial-sample-remove.asciidoc +++ /dev/null @@ -1,18 +0,0 @@ -[[tutorial-sample-remove]] -=== Wrapping up - -When you’re done experimenting with the sample data set, you can remove it. - -. Go to the {kib} home page and click the link next to *Sample data*. -. On the *Sample flight data* card, click *Remove*. - -Now that you have a handle on the {kib} basics, you might be interested in: - -* <>. You’ll learn how to load your own -data, define an index pattern, and create visualizations and dashboards. -* <>. You’ll find information about all the visualization types -{kib} has to offer. -* <>. You have the ability to share a dashboard, or embed the dashboard in a web page. -* <>. You'll learn more about searching data and filtering by field. - - diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc index dda72467f941..5e61475cf283 100644 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ b/docs/getting-started/tutorial-visualizing.asciidoc @@ -1,46 +1,48 @@ [[tutorial-visualizing]] -=== Visualizing your data +=== Visualize your data In the Visualize application, you can shape your data using a variety -of charts, tables, and maps, and more. You'll create four -visualizations: a pie chart, bar chart, coordinate map, and Markdown widget. +of charts, tables, and maps, and more. In this tutorial, you'll create four +visualizations: -. Open *Visualize.* -. Click *Create a visualization* or the *+* button. You'll see all the visualization +* <> +* <> +* <> +* <> + +[float] +[[tutorial-visualize-pie]] +=== Pie chart + +You'll use the pie chart to +gain insight into the account balances in the bank account data. + +. Open *Visualize* to show the overview page. +. Click *Create new visualization*. You'll see all the visualization types in Kibana. + [role="screenshot"] image::images/tutorial-visualize-wizard-step-1.png[] . Click *Pie*. -. In *New Search*, select the `ba*` index pattern. You'll use the pie chart to -gain insight into the account balances in the bank account data. +. In *Choose a source*, select the `ba*` index pattern. + -[role="screenshot"] -image::images/tutorial-visualize-wizard-step-2.png[] - -=== Pie chart - Initially, the pie contains a single "slice." That's because the default search matched all documents. - -[role="screenshot"] -image::images/tutorial-visualize-pie-1.png[] - ++ 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, also known as _buckets_. - -Use a bucket aggregation to establish +categories. You'll use a bucket aggregation to establish multiple ranges of account balances and find out how many accounts fall into each range. -. In the *Buckets* pane, click *Split Slices.* -. In the *Aggregation* dropdown menu, select *Range*. -. In the *Field* dropdown menu, select *balance*. -. Click *Add Range* four times to bring the total number of ranges to six. -. Define the following ranges: +. In the *Buckets* pane, click *Add > Split slices.* ++ +.. In the *Aggregation* dropdown, select *Range*. +.. In the *Field* dropdown, select *balance*. +.. Click *Add range* four times to bring the total number of ranges to six. +.. Define the following ranges: + [source,text] 0 999 @@ -51,120 +53,117 @@ each range. 31000 50000 . Click *Apply changes* image:images/apply-changes-button.png[]. - ++ Now you can see what proportion of the 1000 accounts fall into each balance range. - ++ [role="screenshot"] image::images/tutorial-visualize-pie-2.png[] -Add another bucket aggregation that looks at the ages of the account +. Add another bucket aggregation that looks at the ages of the account holders. -. At the bottom of the *Buckets* pane, click *Add sub-buckets*. -. In *Select buckets type,* click *Split Slices*. -. In the *Sub Aggregation* dropdown, select *Terms*. -. In the *Field* dropdown, select *age*. -. Click *Apply changes* image:images/apply-changes-button.png[]. +.. At the bottom of the *Buckets* pane, click *Add*. +.. For *sub-bucket type,* select *Split slices*. +.. In the *Sub aggregation* dropdown, select *Terms*. +.. In the *Field* dropdown, select *age*. +. Click *Apply changes* image:images/apply-changes-button.png[]. ++ Now you can see the break down of the ages of the account holders, displayed in a ring around the balance ranges. - ++ [role="screenshot"] image::images/tutorial-visualize-pie-3.png[] -To save this chart so you can use it later: - -Click *Save* in the top menu bar and enter `Pie Example`. +. To save this chart so you can use it later, click *Save* in +the top menu bar and enter `Pie Example`. +[float] +[[tutorial-visualize-bar]] === Bar chart You'll use a bar chart to look at the Shakespeare data set and compare the number of speaking parts in the plays. -Create a *Vertical Bar* chart and set the search source to `shakes*`. - +. Create a *Vertical Bar* chart and set the search source to `shakes*`. ++ Initially, the chart is a single bar that shows the total count of documents that match the default wildcard query. -[role="screenshot"] -image::images/tutorial-visualize-bar-1.png[] +. Show the number of speaking parts per play along the Y-axis. -Show the number of speaking parts per play along the Y-axis. -This requires you to configure the Y-axis -{ref}/search-aggregations.html[metric aggregation.] -This aggregation computes metrics based on values from the search results. +.. In the *Metrics* pane, expand *Y-axis*. +.. Set *Aggregation* to *Unique Count*. +.. Set *Field* to *speaker*. +.. In the *Custom label* box, enter `Speaking Parts`. -. In the *Metrics* pane, expand *Y-Axis*. -. Set *Aggregation* to *Unique Count*. -. Set *Field* to *speaker*. -. In the *Custom Label* box, enter `Speaking Parts`. . Click *Apply changes* image:images/apply-changes-button.png[]. +. Show the plays along the X-axis. -[role="screenshot"] -image::images/tutorial-visualize-bar-1.5.png[] - +.. In the *Buckets* pane, click *Add > X-axis*. +.. Set *Aggregation* to *Terms*. +.. Set *Field* to *play_name*. +.. To list plays alphabetically, in the *Order* dropdown, select *Ascending*. +.. Give the axis a custom label, `Play Name`. -Show the plays along the X-axis. - -. In the *Buckets* pane, click *X-Axis*. -. Set *Aggregation* to *Terms* and *Field* to *play_name*. -. To list plays alphabetically, in the *Order* dropdown menu, select *Ascending*. -. Give the axis a custom label, `Play Name`. . Click *Apply changes* image:images/apply-changes-button.png[]. - ++ +[role="screenshot"] +image::images/tutorial-visualize-bar-1.5.png[] +. *Save* this 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`. -*Save* this chart with the name `Bar Example`. - +[float] +[[tutorial-visualize-map]] === Coordinate map Using a coordinate map, you can visualize geographic information in the log file sample data. . Create a *Coordinate map* and set the search source to `logstash*`. -. In the top menu bar, click the time picker on the far right. -. Click *Absolute*. -. Set the start time to May 18, 2015 and the end time to May 20, 2015. -. Click *Go*. - ++ You haven't defined any buckets yet, so the visualization is a map of the world. -[role="screenshot"] -image::images/tutorial-visualize-map-1.png[] +. Set the time. +.. In the time filter, click *Show dates*. +.. Click the start date, then *Absolute*. +.. Set the *Start date* to May 18, 2015. +.. In the time filter, click *now*, then *Absolute*. +.. Set the *End date* to May 20, 2015. -Now map the geo coordinates from the log files. +. Map the geo coordinates from the log files. -. In the *Buckets* pane, click *Geo Coordinates*. -. Set *Aggregation* to *Geohash* and *Field* to *geo.coordinates*. -. Click *Apply changes* image:images/apply-changes-button.png[]. +.. In the *Buckets* pane, click *Add > Geo coordinates*. +.. Set *Aggregation* to *Geohash*. +.. Set *Field* to *geo.coordinates*. +. Click *Apply changes* image:images/apply-changes-button.png[]. ++ The map now looks like this: - ++ [role="screenshot"] image::images/tutorial-visualize-map-2.png[] -You can navigate the map by clicking and dragging. The controls -on the top left of the map enable you to zoom the map and set filters. -Give them a try. - -[role="screenshot"] -image::images/tutorial-visualize-map-3.png[] - -*Save* this map with the name `Map Example`. +. Navigate the map by clicking and dragging. Use the controls +on the left to zoom the map and set filters. +. *Save* this map with the name `Map Example`. +[float] +[[tutorial-visualize-markdown]] === Markdown The final visualization is a Markdown widget that renders formatted text. . Create a *Markdown* visualization. -. In the text box, enter the following: +. Copy the following text into the text box. + [source,markdown] # This is a tutorial dashboard! @@ -172,10 +171,10 @@ The Markdown widget uses **markdown** syntax. > Blockquotes in Markdown use the > character. . Click *Apply changes* image:images/apply-changes-button.png[]. - -The Markdown renders in the preview pane: - ++ +The Markdown renders in the preview pane. ++ [role="screenshot"] image::images/tutorial-visualize-md-2.png[] -*Save* this visualization with the name `Markdown Example`. +. *Save* this visualization with the name `Markdown Example`. diff --git a/docs/getting-started/wrapping-up.asciidoc b/docs/getting-started/wrapping-up.asciidoc deleted file mode 100644 index d2801dad89db..000000000000 --- a/docs/getting-started/wrapping-up.asciidoc +++ /dev/null @@ -1,14 +0,0 @@ -[[wrapping-up]] -=== Wrapping up - -Now that you have a handle on the basics, you're ready to start exploring -your own data with Kibana. - -* See {kibana-ref}/discover.html[Discover] for information about searching and filtering -your data. -* See {kibana-ref}/visualize.html[Visualize] for information about the visualization -types Kibana has to offer. -* See {kibana-ref}/management.html[Management] for information about configuring Kibana -and managing your saved objects. -* See {kibana-ref}/console-kibana.html[Console] to learn about the interactive -console you can use to submit REST requests to Elasticsearch. diff --git a/docs/images/Dashboard_Resize_Menu.png b/docs/images/Dashboard_Resize_Menu.png index 317ae381db0d..835d23afe40e 100644 Binary files a/docs/images/Dashboard_Resize_Menu.png and b/docs/images/Dashboard_Resize_Menu.png differ diff --git a/docs/images/Dashboard_add_visualization.png b/docs/images/Dashboard_add_visualization.png index 957e828eff46..bc705b66e17d 100644 Binary files a/docs/images/Dashboard_add_visualization.png and b/docs/images/Dashboard_add_visualization.png differ diff --git a/docs/images/Dashboard_example.png b/docs/images/Dashboard_example.png index bf226f48944b..5d18acb67bef 100644 Binary files a/docs/images/Dashboard_example.png and b/docs/images/Dashboard_example.png differ diff --git a/docs/images/Dashboard_inspect.png b/docs/images/Dashboard_inspect.png new file mode 100644 index 000000000000..80edcf3a49ca Binary files /dev/null and b/docs/images/Dashboard_inspect.png differ diff --git a/docs/images/add-sample-data.png b/docs/images/add-sample-data.png index 8c927cdde3ac..6e771580b1e2 100644 Binary files a/docs/images/add-sample-data.png and b/docs/images/add-sample-data.png differ diff --git a/docs/images/apply-changes-button.png b/docs/images/apply-changes-button.png index 7ec98e6ccdcb..8625a87d1b2e 100644 Binary files a/docs/images/apply-changes-button.png and b/docs/images/apply-changes-button.png differ diff --git a/docs/images/management_create_rollup_job.png b/docs/images/management_create_rollup_job.png old mode 100644 new mode 100755 index 398105e7af49..f06ce9701084 Binary files a/docs/images/management_create_rollup_job.png and b/docs/images/management_create_rollup_job.png differ diff --git a/docs/images/management_create_rollup_menu.png b/docs/images/management_create_rollup_menu.png old mode 100644 new mode 100755 index 21e19af0c90f..c34dac5f3074 Binary files a/docs/images/management_create_rollup_menu.png and b/docs/images/management_create_rollup_menu.png differ diff --git a/docs/images/management_rolled_dashboard.png b/docs/images/management_rolled_dashboard.png old mode 100644 new mode 100755 index b6f797147781..db731420fb96 Binary files a/docs/images/management_rolled_dashboard.png and b/docs/images/management_rolled_dashboard.png differ diff --git a/docs/images/management_rollup_job_dashboard.png b/docs/images/management_rollup_job_dashboard.png new file mode 100755 index 000000000000..995fde2060ff Binary files /dev/null and b/docs/images/management_rollup_job_dashboard.png differ diff --git a/docs/images/management_rollup_job_details.png b/docs/images/management_rollup_job_details.png old mode 100644 new mode 100755 index 03983977239d..63114adb8d63 Binary files a/docs/images/management_rollup_job_details.png and b/docs/images/management_rollup_job_details.png differ diff --git a/docs/images/management_rollup_job_vis.png b/docs/images/management_rollup_job_vis.png new file mode 100755 index 000000000000..672a3045b335 Binary files /dev/null and b/docs/images/management_rollup_job_vis.png differ diff --git a/docs/images/management_rollup_list.png b/docs/images/management_rollup_list.png old mode 100644 new mode 100755 index 0ca3ce9940fe..bbebb6140d1e Binary files a/docs/images/management_rollup_list.png and b/docs/images/management_rollup_list.png differ diff --git a/docs/images/management_rollups_visualization.png b/docs/images/management_rollups_visualization.png old mode 100644 new mode 100755 index d2c1adb67b94..bba3b6e91a95 Binary files a/docs/images/management_rollups_visualization.png and b/docs/images/management_rollups_visualization.png differ diff --git a/docs/images/monitoring-containers.png b/docs/images/monitoring-containers.png deleted file mode 100644 index ca5e4d0c86ab..000000000000 Binary files a/docs/images/monitoring-containers.png and /dev/null differ diff --git a/docs/images/tutorial-dashboard.png b/docs/images/tutorial-dashboard.png index 5d4056cf7b11..48e75260e9f6 100644 Binary files a/docs/images/tutorial-dashboard.png and b/docs/images/tutorial-dashboard.png differ diff --git a/docs/images/tutorial-discover-2.png b/docs/images/tutorial-discover-2.png index 338d58e1da21..4f4b2dc920cc 100644 Binary files a/docs/images/tutorial-discover-2.png and b/docs/images/tutorial-discover-2.png differ diff --git a/docs/images/tutorial-discover-3.png b/docs/images/tutorial-discover-3.png index 7531accd9a7e..7b3e12d74686 100644 Binary files a/docs/images/tutorial-discover-3.png and b/docs/images/tutorial-discover-3.png differ diff --git a/docs/images/tutorial-full-inspect1.png b/docs/images/tutorial-full-inspect1.png index be76ca1acc8c..8e756634af76 100644 Binary files a/docs/images/tutorial-full-inspect1.png and b/docs/images/tutorial-full-inspect1.png differ diff --git a/docs/images/tutorial-pattern-1.png b/docs/images/tutorial-pattern-1.png index 651dcba33aaa..8a289f93fc66 100644 Binary files a/docs/images/tutorial-pattern-1.png and b/docs/images/tutorial-pattern-1.png differ diff --git a/docs/images/tutorial-sample-dashboard.png b/docs/images/tutorial-sample-dashboard.png index 654496420acd..9f287640f201 100644 Binary files a/docs/images/tutorial-sample-dashboard.png and b/docs/images/tutorial-sample-dashboard.png differ diff --git a/docs/images/tutorial-sample-discover-2.png b/docs/images/tutorial-sample-discover-2.png new file mode 100644 index 000000000000..4f4b2dc920cc Binary files /dev/null and b/docs/images/tutorial-sample-discover-2.png differ diff --git a/docs/images/tutorial-sample-discover1.png b/docs/images/tutorial-sample-discover1.png index defc97c3ea0e..dc35ec41609e 100644 Binary files a/docs/images/tutorial-sample-discover1.png and b/docs/images/tutorial-sample-discover1.png differ diff --git a/docs/images/tutorial-sample-discover2.png b/docs/images/tutorial-sample-discover2.png index f5e55b2b480b..c5d7833db612 100644 Binary files a/docs/images/tutorial-sample-discover2.png and b/docs/images/tutorial-sample-discover2.png differ diff --git a/docs/images/tutorial-sample-edit1.png b/docs/images/tutorial-sample-edit1.png index bf3605ee6d74..621fa3912085 100644 Binary files a/docs/images/tutorial-sample-edit1.png and b/docs/images/tutorial-sample-edit1.png differ diff --git a/docs/images/tutorial-sample-edit2.png b/docs/images/tutorial-sample-edit2.png index 47eb10d718d2..c289b3643a87 100644 Binary files a/docs/images/tutorial-sample-edit2.png and b/docs/images/tutorial-sample-edit2.png differ diff --git a/docs/images/tutorial-sample-filter.png b/docs/images/tutorial-sample-filter.png index ef3cbd0a520f..7c1d04144855 100644 Binary files a/docs/images/tutorial-sample-filter.png and b/docs/images/tutorial-sample-filter.png differ diff --git a/docs/images/tutorial-sample-inspect1.png b/docs/images/tutorial-sample-inspect1.png index ceba1ed3de59..71a608597338 100644 Binary files a/docs/images/tutorial-sample-inspect1.png and b/docs/images/tutorial-sample-inspect1.png differ diff --git a/docs/images/tutorial-sample-query.png b/docs/images/tutorial-sample-query.png index f7312acc6a7e..847542c0b17f 100644 Binary files a/docs/images/tutorial-sample-query.png and b/docs/images/tutorial-sample-query.png differ diff --git a/docs/images/tutorial-visualize-bar-1.5.png b/docs/images/tutorial-visualize-bar-1.5.png index e6d5de20edb1..4ec256959f14 100644 Binary files a/docs/images/tutorial-visualize-bar-1.5.png and b/docs/images/tutorial-visualize-bar-1.5.png differ diff --git a/docs/images/tutorial-visualize-bar-1.png b/docs/images/tutorial-visualize-bar-1.png deleted file mode 100644 index 1aad4f16a492..000000000000 Binary files a/docs/images/tutorial-visualize-bar-1.png and /dev/null differ diff --git a/docs/images/tutorial-visualize-bar-2.png b/docs/images/tutorial-visualize-bar-2.png deleted file mode 100644 index 244d4960ebed..000000000000 Binary files a/docs/images/tutorial-visualize-bar-2.png and /dev/null differ diff --git a/docs/images/tutorial-visualize-map-1.png b/docs/images/tutorial-visualize-map-1.png deleted file mode 100644 index d9d8933d343a..000000000000 Binary files a/docs/images/tutorial-visualize-map-1.png and /dev/null differ diff --git a/docs/images/tutorial-visualize-map-2.png b/docs/images/tutorial-visualize-map-2.png index 9337bcdecd0e..db9f0d56bc96 100644 Binary files a/docs/images/tutorial-visualize-map-2.png and b/docs/images/tutorial-visualize-map-2.png differ diff --git a/docs/images/tutorial-visualize-map-3.png b/docs/images/tutorial-visualize-map-3.png deleted file mode 100644 index be3a8a83c4de..000000000000 Binary files a/docs/images/tutorial-visualize-map-3.png and /dev/null differ diff --git a/docs/images/tutorial-visualize-md-2.png b/docs/images/tutorial-visualize-md-2.png index ec6d6f0278da..9e9a670ba196 100644 Binary files a/docs/images/tutorial-visualize-md-2.png and b/docs/images/tutorial-visualize-md-2.png differ diff --git a/docs/images/tutorial-visualize-pie-1.png b/docs/images/tutorial-visualize-pie-1.png index 229b061d0063..109829c01f28 100644 Binary files a/docs/images/tutorial-visualize-pie-1.png and b/docs/images/tutorial-visualize-pie-1.png differ diff --git a/docs/images/tutorial-visualize-pie-2.png b/docs/images/tutorial-visualize-pie-2.png index b4d41d996db6..6d16c2dab2bf 100644 Binary files a/docs/images/tutorial-visualize-pie-2.png and b/docs/images/tutorial-visualize-pie-2.png differ diff --git a/docs/images/tutorial-visualize-pie-3.png b/docs/images/tutorial-visualize-pie-3.png index 4fb0c2c5ef45..324d8f8c07a2 100644 Binary files a/docs/images/tutorial-visualize-pie-3.png and b/docs/images/tutorial-visualize-pie-3.png differ diff --git a/docs/images/tutorial-visualize-wizard-step-1.png b/docs/images/tutorial-visualize-wizard-step-1.png index f9aa5635fb19..fa353ae52831 100644 Binary files a/docs/images/tutorial-visualize-wizard-step-1.png and b/docs/images/tutorial-visualize-wizard-step-1.png differ diff --git a/docs/images/tutorial-visualize-wizard-step-2.png b/docs/images/tutorial-visualize-wizard-step-2.png deleted file mode 100644 index 32a915e42233..000000000000 Binary files a/docs/images/tutorial-visualize-wizard-step-2.png and /dev/null differ diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 8ee26acd3667..db8e095b6e9a 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -70,8 +70,6 @@ include::management/watcher-ui/index.asciidoc[] include::management/upgrade-assistant/index.asciidoc[] -include::management/dashboard_only_mode/index.asciidoc[] - include::reporting/index.asciidoc[] include::api.asciidoc[] diff --git a/docs/infrastructure/metrics-explorer.asciidoc b/docs/infrastructure/metrics-explorer.asciidoc index 82520e99b42b..008f7ee1bacc 100644 --- a/docs/infrastructure/metrics-explorer.asciidoc +++ b/docs/infrastructure/metrics-explorer.asciidoc @@ -1,58 +1,70 @@ [role="xpack"] [[metrics-explorer]] -The metrics explorer allows you to easily visualize Metricbeat data and group it by arbitary attributes. This empowers you to visualize multiple metrics and can be a jumping off point for further investigations. +== Metrics Explorer + +Metrics Explorer allows you to visualize metrics data collected by Metricbeat and group it in various ways to visualize multiple metrics. +It can be a starting point for further investigations. [role="screenshot"] image::infrastructure/images/metrics-explorer-screen.png[Metrics Explorer in Kibana] [float] [[metrics-explorer-requirements]] -=== Metrics explorer requirements and considerations +=== Metrics Explorer requirements and considerations -* The Metric explorer assumes you have data collected from {metricbeat-ref}/metricbeat-overview.html[Metricbeat]. -* You will need read permissions on `metricbeat-*` or the metric index specified in the Infrastructure configuration UI. -* Metrics explorer uses the timestamp field set in the Infrastructure configuration UI. By default that is set to `@timestmap`. -* The interval for the X Axis is set to `auto`. The bucket size is determined by the time range. -* **Open in Visualize** requires the user to have access to the Visualize app, otherwise it will not be available. +* The Metrics Explorer uses data collected from {metricbeat-ref}/metricbeat-overview.html[Metricbeat]. +* You need read permissions on `metricbeat-*` or the metric index specified in the Infrastructure configuration UI. +* Metrics Explorer uses the timestamp field set in the Infrastructure configuration UI. +By default that is set to `@timestamp`. +* The interval for the X Axis is set to `auto`. +The bucket size is determined by the time range. +* *Open in Visualize* requires you to have access to the Visualize app, otherwise it is not available. [float] [[metrics-explorer-tutorial]] -=== Metrics explorer tutorial - -In this tutorial we are going to use the Metrics explorer to create system load charts for each host we are monitoring with Metricbeat. -Once we've explored the system load metrics, -we'll show you how to filter down to a specific host and start exploring outbound network traffic for each interface. -Before we get started, if you don't have any Metricbeat data, you'll need to head over to our -{metricbeat-ref}/metricbeat-overview.html[Metricbeat documentation] and learn how to install and start collection. - -1. Navigate to the Infrastructure UI in Kibana and select **Metrics Explorer** -The initial screen should be empty with the metric field selection open. -2. Start typing `system.load.1` and select the field. -Once you've selected the field, you can add additional metrics for `system.load.5` and `system.load.15`. -3. You should now have a chart with 3 different series for each metric. -By default, the metric explorer will take the average of each field. -To the left of the metric dropdown you will see the aggregation dropdown. -You can use this to change the aggregation. -For now, we'll leave it set to `Average`, but take some time to play around with the different aggregations. -4. To the right of the metric input field you will see **graph per** and a dropdown. -Enter `host.name` in this dropdown and select the field. -This input will create a chart for every value it finds in the selected field. -5. By now, your UI should look similar to the screenshot above. -If you only have one host, then it will display the chart across the entire screen. -For multiple hosts, the metric explorer divides the screen into three columns. -Configurations, you've explored your first metric! -6. Let's go for some bonus points. Select the **Actions** dropdown in the upper right hand corner of one of the charts. -Select **Add Filter** to change the KQL expression to filter for that specific host. -From here we can start exploring other metrics specific to this host. -7. Let's delete each of the system load metrics by clicking the little **X** icon next to each of them. -8. Set `system.network.out.bytes` as the metric. -Because `system.network.out.bytes` is a monotonically increasing number, we need to change the aggregation to `Rate`. -While this chart might appear correct, there is one critical problem: hosts have multiple interfaces. -9. To fix our chart, set the group by dropdown to `system.network.name`. -You should now see a chart per network interface. -10. Let's imagine you want to put one of these charts on a dashboard. -Click the **Actions** menu next to one of the interface charts and select **Open In Visualize**. -This will open the same chart in Time Series Visual Builder. From here you can save the chart and add it to a dashboard. - -Who's the Metrics explorer now? You are! +=== Metrics Explorer tutorial + +In this tutorial we'll use Metrics Explorer to view the system load metrics for each host we're monitoring with Metricbeat. +After that, we'll filter down to a specific host and explore the outbound traffic for each network interface. +Before we start, if you don't have any Metricbeat data, you'll need to head over to our +{metricbeat-ref}/metricbeat-overview.html[Metricbeat documentation] to install Metricbeat and start collecting data. + +1. When you have Metricbeat running and collecting data, open Kibana and navigate to *Infrastructure*. +The *Inventory* tab shows the host or hosts you are monitoring. + +2. Select the *Metrics Explorer* tab. +The initial configuration has the *Average* aggregation selected, the *of* field populated with some default metrics, and the *graph per* dropdown set to `Everything`. + +3. To select the metrics to view, firstly delete all the metrics currently shown in the *of* field by clicking the *X* by each metric name. +Then, in this field, start typing `system.load.1` and select this metric. +Also add metrics for `system.load.5` and `system.load.15`. +You will see a graph showing the average values of the metrics you selected. +In this step we'll leave the aggregation dropdown set to *Average* but you can try different values later if you like. + +4. In the *graph per* dropdown, enter `host.name` and select this field. +You will see a separate graph for each host you are monitoring. +If you are collecting metrics for multiple hosts, you will see something like the screenshot above. +If you only have metrics for a single host, you will see a single graph. +Congratulations! Either way, you've explored your first metric. + +5. Let's explore a bit further. +In the upper right hand corner of the graph for one of the hosts, select the *Actions* dropdown and click *Add Filter* to show ony the metrics for that host. +This adds a {kibana-ref}/kuery-query.html[Kibana Query Language] filter for `host.name` in the second row of the Metrics Explorer configuration. +If you only have one host, the graph will not change as you are already exploring metrics for a single host. + +6. Now you can start exploring some host-specific metrics. +First, delete each of the system load metrics in the *of* field by clicking the *X* by the metric name. +Then enter the metric `system.network.out.bytes` to explore the outbound network traffic. +This is a monotonically increasing value, so change the aggregation dropdown to `Rate`. + +7. Since hosts have multiple network interfaces, it is more meaningful to display one graph for each network interface. +To do this, select the *graph per* dropdown, start typing `system.network.name` and select this field. +You will now see a separate graph for each network interface. + +8. If you like, you can put one of these graphs in a dashboard. +Choose a graph, click the *Actions* dropdown and select *Open In Visualize*. +This opens the graph in {kibana-ref}/TSVB.html[TSVB]. +From here you can save the graph and add it to a dashboard as usual. + +Who's the Metrics Explorer now? You are! diff --git a/docs/management.asciidoc b/docs/management.asciidoc index 0b1ac4c8ae62..ee8fc0d72e71 100644 --- a/docs/management.asciidoc +++ b/docs/management.asciidoc @@ -17,8 +17,6 @@ include::management/index-patterns.asciidoc[] include::management/rollups/create_and_manage_rollups.asciidoc[] -include::management/rollups/visualize_rollup_data.asciidoc[] - include::management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[] include::management/index-lifecycle-policies/create-policy.asciidoc[] diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 1d6020e02dcc..e78421c40d89 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -43,6 +43,7 @@ removes it from {kib} permanently. `dateFormat:scaled`:: The values that define the format to use to render ordered time-based data. Formatted timestamps must adapt to the interval between measurements. Keys are http://en.wikipedia.org/wiki/ISO_8601#Time_intervals[ISO8601 intervals]. `dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser. +`dateNanosFormat`:: The format to use for displaying https://momentjs.com/docs/#/displaying/format/[pretty formatted dates] of {ref}/date_nanos.html[Elasticsearch date_nanos type]. `defaultIndex`:: The index to access if no index is set. The default is `null`. `fields:popularLimit`:: The top N most popular fields to show. `filterEditor:suggestValues`:: Set this property to `false` to prevent the filter editor from suggesting values for fields. @@ -132,6 +133,8 @@ The default is `_source`. the Visualize button in the field drop down. The default is `20`. `discover:sampleSize`:: The number of rows to show in the Discover table. `discover:sort:defaultOrder`:: The default sort direction for time-based index patterns. +`discover:searchOnPageLoad`:: Controls whether a search is executed when Discover first loads. +This setting does not have an effect when loading a saved search. `doc_table:hideTimeColumn`:: Hides the "Time" column in Discover and in all saved searches on dashboards. `doc_table:highlight`:: Highlights results in Discover and saved searches on dashboards. Highlighting slows requests when @@ -199,7 +202,7 @@ because requests can be spread across all shard copies. However, results might be inconsistent because different shards might be in different refresh states. `search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results. Searching through frozen indices -might increase the search time. +might increase the search time. This setting is off by default. Users must opt-in to include frozen indices. [float] [[kibana-timelion-settings]] diff --git a/docs/management/dashboard_only_mode/advanced_configuration.asciidoc b/docs/management/dashboard_only_mode/advanced_configuration.asciidoc deleted file mode 100644 index 44b59c51cb07..000000000000 --- a/docs/management/dashboard_only_mode/advanced_configuration.asciidoc +++ /dev/null @@ -1,25 +0,0 @@ -[role="xpack"] -[[advanced-dashboard-mode-configuration]] -=== Advanced Configuration for Dashboard Only Mode - -If {security} is enabled, Kibana has a built-in `kibana_dashboard_only_user` -role that grants read-only access to {kib}. This role is sufficient -for most use cases. However, if your setup requires a custom {kib} index, you can create -your own roles and tag them as *Dashboard only mode*. - -Go to *Management > Kibana > Advanced Settings* and search for *Dashboard*. By default -`xpackDashboardMode:roles` is set to `kibana_dashboard_only_user`. -Here you can add as many roles as you like. - -[role="screenshot"] -image:management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png["Advanced dashboard mode role setup"] - -By default, a *dashboard only mode* user doesn't have access to any data indices. -To grant read-only access to your custom {kib} instance, -you must assign the read <>. -These privileges are available under *Management > Security > Roles*. - -For more information on roles and privileges, see {xpack-ref}/authorization.html[User Authorization]. - -[role="screenshot"] -image:management/dashboard_only_mode/images/custom_dashboard_mode_role.png["Custom dashboard mode role with read permissions on a custom kibana index"] diff --git a/docs/management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png b/docs/management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png index a61ce5b4686b..1e9600e4d4eb 100644 Binary files a/docs/management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png and b/docs/management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png differ diff --git a/docs/management/dashboard_only_mode/images/custom_dashboard_mode_role.png b/docs/management/dashboard_only_mode/images/custom_dashboard_mode_role.png index 673ca7b6d94b..e9285ac91430 100644 Binary files a/docs/management/dashboard_only_mode/images/custom_dashboard_mode_role.png and b/docs/management/dashboard_only_mode/images/custom_dashboard_mode_role.png differ diff --git a/docs/management/dashboard_only_mode/images/dashboard-only-user-role.png b/docs/management/dashboard_only_mode/images/dashboard-only-user-role.png new file mode 100644 index 000000000000..0773f0f3a340 Binary files /dev/null and b/docs/management/dashboard_only_mode/images/dashboard-only-user-role.png differ diff --git a/docs/management/dashboard_only_mode/images/view_only_dashboard.png b/docs/management/dashboard_only_mode/images/view_only_dashboard.png index 6b858650638c..a82a09c27e6e 100644 Binary files a/docs/management/dashboard_only_mode/images/view_only_dashboard.png and b/docs/management/dashboard_only_mode/images/view_only_dashboard.png differ diff --git a/docs/management/dashboard_only_mode/index.asciidoc b/docs/management/dashboard_only_mode/index.asciidoc index e9acad28a856..320a123ab108 100644 --- a/docs/management/dashboard_only_mode/index.asciidoc +++ b/docs/management/dashboard_only_mode/index.asciidoc @@ -1,32 +1,85 @@ [role="xpack"] [[xpack-dashboard-only-mode]] -== Kibana Dashboard Only Mode +== Dashboard-only mode -If {security} is enabled, you can use the `kibana_dashboard_only_user` built-in role to limit -what users see when they log in to {kib}. The `kibana_dashboard_only_user` role is -preconfigured with read-only permissions to {kib}. +deprecated[7.4.0, Using the `kibana_dashboard_only_user` role is deprecated. Use <> instead.] -IMPORTANT: You must also assign roles that grant the user appropriate access to the data indices. -For information on roles and privileges, see {xpack-ref}/authorization.html[User Authorization]. +In dashboard-only mode, users have access to only the *Dashboard* app. +Users can view and filter the dashboards, but cannot create, edit, or delete +them. This enables you to: -Users assigned this role are only able to see the Dashboard app in the navigation -pane. When users open a dashboard, they will have a limited visual experience. -All edit and create controls are hidden. +* Show off your dashboards without giving users access to all of {kib} + +* Share your {kib} dashboards without the risk of users accidentally +editing or deleting them + +Dashboard-only mode pairs well with fullscreen mode. +You can share your dashboard with the team responsible +for showing the dashboard on a big-screen monitor, and not worry about it being modified. [role="screenshot"] image:management/dashboard_only_mode/images/view_only_dashboard.png["View Only Dashboard"] -To assign this role, go to *Management > Security > Users*, add or edit -a user, and add the `kibana_dashboard_only_user` role. +[[setup-dashboard-only-mode]] +[float] +=== Assign dashboard-only mode +With {security} enabled, you can restrict users to dashboard-only mode by assigning +them the built-in `kibana_dashboard_only_user` role. + +. Go to *Management > Security > Users*. +. Create or edit a user. +. Assign the `kibana_dashboard_only_user` role and a role that <>. ++ +For example, +to enable users to view the dashboards in the sample data sets, you must assign them +the `kibana_dashboard_only_user` role and a role that has +`read` access to the kibana_* indices. ++ +[role="screenshot"] +image:management/dashboard_only_mode/images/dashboard-only-user-role.png["Dashboard Only mode has no editing controls"] -IMPORTANT: If you assign users the `kibana_dashboard_only_user` role, along with a role +[IMPORTANT] +=========================================== +* If you assign users the `kibana_dashboard_only_user` role and a role with write permissions to {kib}, they *will* have write access, -even though the controls remain hidden in the {kib} UI. +even though the controls remain hidden in {kib}. + +* If you also assign users the reserved `superuser` role, they will have full +access to {kib}. + +=========================================== + +[float] +[[grant-read-access-to-indices]] +=== Grant read access to indices -IMPORTANT: If you also assign users the reserved `superuser` role, they will be able to see -all of {kib} and have full access. +The `kibana_dashboard_only_user` role +does not provide access to data indices. +You must also assign the user a role that grants `read` access +to each index you are using. Use *Management > Security > Roles* to create or edit a +role and assign index privileges. +For information on roles and privileges, see {stack-ov}/authorization.html[User authorization]. -<> that use a -custom {kib} index are possible. +[role="screenshot"] +image:management/dashboard_only_mode/images/custom_dashboard_mode_role.png["Dashboard Only mode has no editing controls"] + + +[float] +[[advanced-dashboard-mode-configuration]] +=== Advanced settings for dashboard only mode + +The `kibana_dashboard_only_user` role grants access to all spaces. +If your setup requires access to a +subset of spaces, you can create a custom role, and then tag it as Dashboard only mode. + +. Go to *Management > Advanced Settings*, and search for `xpackDashboardMode:roles`. ++ +By +default, this is set to +`kibana_dashboard_only_user`. + +. Add as many roles as you require. ++ +[role="screenshot"] +image:management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png["Advanced dashboard mode role setup"] -include::advanced_configuration.asciidoc[] diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index ee25d93e032f..06983c01f926 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -1,90 +1,148 @@ [role="xpack"] [[data-rollups]] -== Working with rollup indices +== Rollup jobs -The {ref}/xpack-rollup.html[rollup feature in {es}] -enables you to summarize historical data and store it compactly for future analysis, -so you can query, aggregate, and visualize the data using a fraction of the storage. -This is a good way to keep costs down when you need to store months or years of -historical data for use in visualizations and reports. -{kib} supports rolled up data in two ways: +A rollup job is a periodic task that aggregates data from indices specified +by an index pattern and rolls it into a new index. Rollup indices are a good way to +compactly store months or years of historical +data for use in visualizations and reports. -* You can create and manage a rollup job in Management -* You can create a visualization using rolled up data in -Visualize and view it in a dashboard +You’ll find *Rollup Jobs* under *Management > Elasticsearch*. With this UI, +you can: - -[role="xpack"] -[[create-and-manage-rollup-job]] -=== Create and manage rollup jobs - -In Management, you'll find a UI for viewing, creating, starting, stopping, and -deleting rollup jobs. A rollup job is a periodic task that summarizes data from -indices specified by an index pattern and rolls it into a new index. To navigate -to the UI, go to *Management*, and under *Elasticsearch*, click *Rollup Jobs*. +* <> +* <> [role="screenshot"] image::images/management_rollup_list.png[][List of currently active rollup jobs] +Before using this feature, you should be familiar with how rollups work. +{ref}/xpack-rollup.html[Rolling up historical data] is a good source for more detailed information. + [float] -[[create-rollup-job]] -==== Creating a rollup job +[[create-and-manage-rollup-job]] +=== Create a rollup job -{kib} makes it easy for you to create a rollup job by walking you through the -process step by step. The first step is to define the job logistics. These include -the name of the rollup job, the index or indices to summarize, and the output rollup index. +{kib} makes it easy for you to create a rollup job by walking you through +the process. You fill in the name, data flow, and how often you want to roll +up the data. Then you define a date histogram aggregation for the rollup job +and optionally terms, histogram, and metrics aggregations. -The index pattern cannot match the name of the output rollup index. For example, -if your index pattern is `metricbeat-*`, you cannot name your rollup index -`metricbeat-rollup`. Otherwise, the job will attempt to capture the data in the -rollup index. +When defining the index pattern, you must enter a name that is different than +the output rollup index. Otherwise, the job +will attempt to capture the data in the rollup index. For example, if your index pattern is `metricbeat-*`, +you can name your rollup index `rollup-metricbeat`, but not `metricbeat-rollup`. [role="screenshot"] image::images/management_create_rollup_job.png[][Wizard that walks you through creation of a rollup job] -You must set a schedule for the rollup job: how often to collect the data, -the number of documents to roll up at a time, and the duration of its latency. -The latency buffer field is provided to protect against the late arrival of data -from Beats or other sources. By delaying the rollup for the specified amount of -time from when the job starts, you allow for the inclusion of late-arriving data -in the rollup. +[float] +[[manage-rollup-job]] +=== Start, stop, and delete rollup jobs -In the subsequent phases, you define the Date Histogram aggregation for the job -and optionally the Terms and Histogram aggregations. +Once you’ve saved a rollup job, you’ll see it the *Rollup Jobs* overview page, +where you can drill down for further investigation. The *Manage* menu in +the lower right enables you to start, stop, and delete the rollup job. +You must first stop a rollup job before deleting it. -* The Date Histogram aggregation defines the time intervals for summarizing the data. -This value is important because you cannot search the data with a smaller value -than this interval. However, you can aggregate buckets in a larger time interval. +[role="screenshot"] +image::images/management_rollup_job_details.png[][Rollup job details] -* The Terms histogram enables you to split the time buckets into sub buckets for -term field values. +You can’t change a rollup job after you’ve created it. To select additional fields +or redefine terms, you must delete the existing job, and then create a new one +with the updated specifications. Be sure to use a different name for the new rollup +job—reusing the same name can lead to problems with mismatched job configurations. +You can read more at {ref}/rollup-job-config.html[rollup job configuration]. -* The Histogram aggregation enables you to split the time buckets into sub buckets -for numeric field values. +[float] +=== Try it: Create and visualize rolled up data -The final step is to specify the fields for calculating metrics. For each selected -field, you can collect any or all of the following: value count, average, sum, min, and max. +This example creates a rollup job to capture log data from sample web logs. +To follow along, add the <>. -Before you save the rollup job, {kib} displays a summary of the rollup job for -validation. +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 +rolled up data on an hourly basis, using 60m for the time bucket configuration. +This allows for more granular queries, such as 2h and 12h. [float] -[[manage-rollup-job]] -==== Managing rollup jobs +==== Create the rollup job -Selecting a job on the *Rollup jobs* page shows its details. The Manage menu in -the lower right enables you to start, stop, and delete the rollup job. -You must first stop a rollup job before deleting it. +As you walk through the *Create rollup job* UI, enter the data shown in +the table below. The terms, histogram, and metrics fields reflect +the key information to retain in the rolled up data: where visitors are from (geo.src), +what operating system they are using (machine.os.keyword), +and how much data is being sent (bytes). + +|=== +|*Field* |*Value* + +|Name +|logs_job + +|Index pattern +|`kibana_sample_data_logs` + +|Rollup index name +|`rollup_logstash` + +|Frequency +|Every day at midnight + +|Page size +|1000 + +|Delay (latency buffer)|7d + +|Date field +|@timestamp + +|Time bucket size +|60m + +|Time zone +|UTC + +|Terms +|geo.src, machine.os.keyword + +|Histogram +|bytes, memory + +|Histogram interval +|1000 + +|Metrics +|bytes (average) +|=== + + +You can now use the rolled up data for analysis at a fraction of the storage cost +of the original index. The original data can live side by side with the new +rollup index, or you can remove or archive it using <>. + +[float] +==== Visualize the rolled up data + +Your next step is to visualize your rolled up data in a vertical bar chart. +Most visualizations support rolled up data, with the exception of Timelion, TSVB, and Vega visualizations. + +Using the information from the example rollup configuration described above, +you can use `rollup_logstash` to match the rolled up index pattern, +and `kibana_sample_data_logs` to match the index pattern for raw data. +The notation for a combination index pattern with both raw and rolled up data +is `rollup_logstash,kibana_sample_data_logs`. [role="screenshot"] -image::images/management_rollup_job_details.png[][Rollup job details] +image::images/management_rollup_job_vis.png[][Visualization of rolled up data] + +You can then create a dashboard that contains visualizations of the rolled up +data, raw data, or both. See <> +for more information. + +[role="screenshot"] +image::images/management_rollup_job_dashboard.png[][Dashboard with rolled up data] -You can start, stop, and delete an existing rollup job, but edits are not supported. -If you want to make any changes, delete the existing job and create a new one with -the updated specifications. Be sure to use a different name for the new rollup job; -reusing the same name could lead to problems with mismatched job configurations. -More about logistical details for the {ref}/rollup-job-config.html[rollup job configuration] -can be found in the {es} documentation. diff --git a/docs/management/rollups/visualize_rollup_data.asciidoc b/docs/management/rollups/visualize_rollup_data.asciidoc deleted file mode 100644 index 5f64e4bdd0ac..000000000000 --- a/docs/management/rollups/visualize_rollup_data.asciidoc +++ /dev/null @@ -1,54 +0,0 @@ -[role="xpack"] -[[visualize-rollup-data]] -=== Create a visualization using rolled up data - -beta[] - -You can visualize your rolled up data in a variety of charts, tables, maps, and -more. Most visualizations support rolled up data, with the exception of -Timelion, TSVB, and Vega visualizations. - -You create an index pattern for rolled up data the same way you do for any data, -in *Management > Kibana > Index patterns*. Clicking *Create index pattern* includes -an item for creating a rollup index pattern, if a rollup index is detected in the cluster. - -[role="screenshot"] -image::images/management_create_rollup_menu.png[Create index pattern menu] - -You can match an index pattern to only rolled up data, or mix both rolled up -and raw data to visualize all data together. An index pattern can match only one -rolled up index, not multiple. There is no restriction on the number of standard -indices that an index pattern can match. - -Combination index patterns use the same -notation as other multiple indices in {es}. To match multiple indices to create a -combination index pattern, use a comma to separate the names, with no space after the comma. -The notation for wildcards (`*`) and the ability to "exclude" (`-`) also apply -(for example, `test*,-test3`). - -When creating an index pattern, you’re asked to set a time field for filtering. -With a rollup index, the time filter field is the same field used for -the rolled up date histogram aggregation. - -Keep the following in mind when creating a visualization from rolled up data: - -* The data in a rollup index only has summarized metrics for specific fields. -You can’t search any other field from the original raw data. -* Data is summarized into time buckets that might be split into sub buckets for -numeric field values or terms. You can ask for a time aggregation that takes -several time buckets and combines them to lower granularity. For example, -if the rollup job was aggregated by hours, you can ask for buckets of days. - -The data represented in this visualization comes from a rollup index and -standard indices. - -[role="screenshot"] -image::images/management_rollups_visualization.png[][Rollups in visualizations] - -You can mix rollup visualizations and regular visualizations in a dashboard. -The following dashboard shows this mix, along with a field filter. Note -that not all queries and filters are supported by rollups. - -[role="screenshot"] -image::images/management_rolled_dashboard.png[][Rollups in dashboards] - diff --git a/docs/maps/images/read-only-badge.png b/docs/maps/images/read-only-badge.png new file mode 100644 index 000000000000..50289ea80f60 Binary files /dev/null and b/docs/maps/images/read-only-badge.png differ diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 4925ec49897d..cec193888141 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -13,6 +13,16 @@ light to dark. [role="screenshot"] image::maps/images/sample_data_web_logs.png[] +[float] +[[maps-read-only-access]] +NOTE: If you have insufficient privileges to create or save maps, a read-only icon +appears in the application header. The buttons to create new maps or edit +existing maps won't be visible. For more information on granting access to +Kibana see <>. + +[role="screenshot"] +image::maps/images/read-only-badge.png[Example of Maps' read only access indicator in Kibana's header] + [float] === Prerequisites Before you start this tutorial, <>. Each diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 9598c33dfaa1..e833911fab2d 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -36,6 +36,12 @@ for example, `logstash-*`. === Settings changes // tag::notable-breaking-changes[] +[float] +==== Legacy browsers are now rejected by default +*Details:* `csp.strict` is now enabled by default, so Kibana will fail to load for older, legacy browsers that do not enforce basic Content Security Policy protections - notably Internet Explorer 11. + +*Impact:* To allow Kibana to function for these legacy browsers, set `csp.strict: false`. Since this is about enforcing a security protocol, we *strongly discourage* disabling `csp.strict` unless it is critical that you support Internet Explorer 11. + [float] ==== Default logging timezone is now the system's timezone *Details:* In prior releases the timezone used in logs defaulted to UTC. We now use the host machine's timezone by default. diff --git a/docs/ml/creating-df-kib.asciidoc b/docs/ml/creating-df-kib.asciidoc index 9c51fd29bc64..872c9d5dda8b 100644 --- a/docs/ml/creating-df-kib.asciidoc +++ b/docs/ml/creating-df-kib.asciidoc @@ -1,3 +1,4 @@ +[role="xpack"] [[creating-df-kib]] == Creating {dataframe-transforms} diff --git a/docs/ml/creating-jobs.asciidoc b/docs/ml/creating-jobs.asciidoc index e3bdde651468..98f175041719 100644 --- a/docs/ml/creating-jobs.asciidoc +++ b/docs/ml/creating-jobs.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[ml-jobs]] -== Creating machine learning jobs +== Creating {anomaly-jobs} -Machine learning jobs contain the configuration information and metadata +{anomaly-jobs-cap} contain the configuration information and metadata necessary to perform an analytics task. {kib} provides the following wizards to make it easier to create jobs: @@ -33,7 +33,7 @@ appears: [role="screenshot"] image::ml/images/ml-data-recognizer-sample.jpg[A screenshot of the {kib} sample data web log job creation wizard] -TIP: Alternatively, after you load a sample data set on the {kib} home page, you can click *View data* > *ML jobs*. There are {ml} jobs for both the sample eCommerce orders data set and the sample web logs data set. +TIP: Alternatively, after you load a sample data set on the {kib} home page, you can click *View data* > *ML jobs*. There are {anomaly-jobs} for both the sample eCommerce orders data set and the sample web logs data set. If you use {filebeat-ref}/index.html[{filebeat}] to ship access logs from your @@ -57,17 +57,17 @@ wizards appear: [role="screenshot"] image::ml/images/ml-data-recognizer-metricbeat.jpg[A screenshot of the {metricbeat} job creation wizards] -These wizards create {ml} jobs, dashboards, searches, and visualizations that -are customized to help you analyze your {auditbeat}, {filebeat}, and +These wizards create {anomaly-jobs}, dashboards, searches, and visualizations +that are customized to help you analyze your {auditbeat}, {filebeat}, and {metricbeat} data. [NOTE] =============================== If your data is located outside of {es}, you cannot use {kib} to create your jobs and you cannot use {dfeeds} to retrieve your data in real time. -Machine learning analysis is still possible, however, by using APIs to +{anomal-detect-cap} is still possible, however, by using APIs to create and manage jobs and post data to them. For more information, see -{ref}/ml-apis.html[Machine Learning APIs]. +{ref}/ml-apis.html[{ml-cap} {anomaly-detect} APIs]. =============================== //// diff --git a/docs/ml/index.asciidoc b/docs/ml/index.asciidoc index 4c3c5d461789..eac51d06a51f 100644 --- a/docs/ml/index.asciidoc +++ b/docs/ml/index.asciidoc @@ -1,35 +1,36 @@ [role="xpack"] [[xpack-ml]] -= Machine Learning += {ml-cap} [partintro] -- As datasets increase in size and complexity, the human effort required to inspect dashboards or maintain rules for spotting infrastructure problems, -cyber attacks, or business issues becomes impractical. The Elastic {ml-features} -automatically model the normal behavior of your time series data — learning -trends, periodicity, and more — in real time to identify anomalies, streamline -root cause analysis, and reduce false positives. +cyber attacks, or business issues becomes impractical. The Elastic {ml} +{anomaly-detect} feature automatically models the normal behavior of your time +series data — learning trends, periodicity, and more — in real time to identify +anomalies, streamline root cause analysis, and reduce false positives. -The {ml-features} run in and scale with {es}, and include an -intuitive UI on the {kib} *Machine Learning* page for creating anomaly detection -jobs and understanding results. +{anomaly-detect-cap} runs in and scales with {es}, and includes an +intuitive UI on the {kib} *Machine Learning* page for creating {anomaly-jobs} +and understanding results. If you have a basic license, you can use the *Data Visualizer* to learn more about your data. In particular, if your data is stored in {es} and contains a time field, you can use the *Data Visualizer* to identify possible fields for -{ml} analysis: +{anomaly-detect}: [role="screenshot"] image::ml/images/ml-data-visualizer-sample.jpg[Data Visualizer for sample flight data] -experimental[] You can also upload a CSV, NDJSON, or log file (up to 100 MB in size). -The {ml-features} identify the file format and field mappings. You can then -optionally import that data into an {es} index. +experimental[] You can also upload a CSV, NDJSON, or log file (up to 100 MB in +size). The *Data Visualizer* identifies the file format and field mappings. You +can then optionally import that data into an {es} index. -If you have a trial or platinum license, you can <> -and manage jobs and {dfeeds} from the *Job Management* pane: +If you have a trial or platinum license, you can +<> and manage jobs and {dfeeds} from the *Job +Management* pane: [role="screenshot"] image::ml/images/ml-job-management.jpg[Job Management] @@ -42,7 +43,7 @@ You can use the *Settings* pane to create and edit image::ml/images/ml-settings.jpg[Calendar Management] The *Anomaly Explorer* and *Single Metric Viewer* display the results of your -{ml} jobs. For example: +{anomaly-jobs}. For example: [role="screenshot"] image::ml/images/ml-single-metric-viewer.jpg[Single Metric Viewer] @@ -56,17 +57,17 @@ occurring in your operational environment at that time: image::ml/images/ml-annotations-list.jpg[Single Metric Viewer with annotations] In some circumstances, annotations are also added automatically. For example, if -the {ml} analytics detect that there is missing data, it annotates the affected +the {anomaly-job} detects that there is missing data, it annotates the affected time period. For more information, see -{stack-ov}/ml-delayed-data-detection.html[Handling delayed data]. -The *Job Management* pane shows the full list of annotations for each job. +{stack-ov}/ml-delayed-data-detection.html[Handling delayed data]. The +*Job Management* pane shows the full list of annotations for each job. -NOTE: The {kib} {ml-features} use pop-ups. You must configure your -web browser so that it does not block pop-up windows or create an exception for -your {kib} URL. +NOTE: The {kib} {ml-features} use pop-ups. You must configure your web +browser so that it does not block pop-up windows or create an exception for your +{kib} URL. -For more information about {ml}, see -{stack-ov}/xpack-ml.html[Machine learning in the {stack}]. +For more information about the {anomaly-detect} feature, see +{stack-ov}/xpack-ml.html[{ml-cap} {anomaly-detect}]. -- diff --git a/docs/ml/job-tips.asciidoc b/docs/ml/job-tips.asciidoc index 2e5df33d727c..3451d45bd17a 100644 --- a/docs/ml/job-tips.asciidoc +++ b/docs/ml/job-tips.asciidoc @@ -5,16 +5,17 @@ Job tips ++++ -When you are creating a job in {kib}, the job creation wizards can provide -advice based on the characteristics of your data. By heeding these suggestions, -you can create jobs that are more likely to produce insightful {ml} results. +When you create an {anomaly-job} in {kib}, the job creation wizards can +provide advice based on the characteristics of your data. By heeding these +suggestions, you can create jobs that are more likely to produce insightful {ml} +results. [[bucket-span]] ==== Bucket span The bucket span is the time interval that {ml} analytics use to summarize and -model data for your job. When you create a job in {kib}, you can choose to -estimate a bucket span value based on your data characteristics. +model data for your job. When you create an {anomaly-job} in {kib}, you can +choose to estimate a bucket span value based on your data characteristics. NOTE: The bucket span must contain a valid time interval. For more information, see {ref}/ml-job-resource.html#ml-analysisconfig[Analysis configuration objects]. @@ -22,7 +23,7 @@ see {ref}/ml-job-resource.html#ml-analysisconfig[Analysis configuration objects] If you choose a value that is larger than one day or is significantly different than the estimated value, you receive an informational message. For more information about choosing an appropriate bucket span, see -{xpack-ref}/ml-buckets.html[Buckets]. +{stack-ov}/ml-buckets.html[Buckets]. [[cardinality]] ==== Cardinality @@ -40,14 +41,14 @@ job uses more memory resources. In particular, if the cardinality of the Likewise if you are performing population analysis and the cardinality of the `over_field_name` is below 10, you are advised that this might not be a suitable field to use. For more information, see -{xpack-ref}/ml-configuring-pop.html[Performing Population Analysis]. +{stack-ov}/ml-configuring-pop.html[Performing Population Analysis]. [[detectors]] ==== Detectors -Each job must have one or more _detectors_. A detector applies an analytical -function to specific fields in your data. If your job does not contain a -detector or the detector does not contain a +Each {anomaly-job} must have one or more _detectors_. A detector applies an +analytical function to specific fields in your data. If your job does not +contain a detector or the detector does not contain a {stack-ov}/ml-functions.html[valid function], you receive an error. If a job contains duplicate detectors, you also receive an error. Detectors are @@ -57,9 +58,9 @@ duplicates if they have the same `function`, `field_name`, `by_field_name`, [[influencers]] ==== Influencers -When you create a job, you can specify _influencers_, which are also sometimes -referred to as _key fields_. Picking an influencer is strongly recommended for -the following reasons: +When you create an {anomaly-job}, you can specify _influencers_, which are also +sometimes referred to as _key fields_. Picking an influencer is strongly +recommended for the following reasons: * It allows you to more easily assign blame for the anomaly * It simplifies and aggregates the results @@ -78,11 +79,11 @@ The job creation wizards in {kib} can suggest which fields to use as influencers [[model-memory-limits]] ==== Model memory limits -For each job, you can optionally specify a `model_memory_limit`, which is the -approximate maximum amount of memory resources that are required for analytical -processing. The default value is 1 GB. Once this limit is approached, data -pruning becomes more aggressive. Upon exceeding this limit, new entities are not -modeled. +For each {anomaly-job}, you can optionally specify a `model_memory_limit`, which +is the approximate maximum amount of memory resources that are required for +analytical processing. The default value is 1 GB. Once this limit is approached, +data pruning becomes more aggressive. Upon exceeding this limit, new entities +are not modeled. You can also optionally specify the `xpack.ml.max_model_memory_limit` setting. By default, it's not set, which means there is no upper bound on the acceptable @@ -92,9 +93,9 @@ TIP: If you set the `model_memory_limit` too high, it will be impossible to open the job; jobs cannot be allocated to nodes that have insufficient memory to run them. -If the estimated model memory limit for a job is greater than the model memory -limit for the job or the maximum model memory limit for the cluster, the job -creation wizards in {kib} generate a warning. If the estimated memory +If the estimated model memory limit for an {anomaly-job} is greater than the +model memory limit for the job or the maximum model memory limit for the cluster, +the job creation wizards in {kib} generate a warning. If the estimated memory requirement is only a little higher than the `model_memory_limit`, the job will probably produce useful results. Otherwise, the actions you take to address these warnings vary depending on the resources available in your cluster: diff --git a/docs/monitoring/cluster-alerts.asciidoc b/docs/monitoring/cluster-alerts.asciidoc index e10e4835f1c4..f36cbaf4d54f 100644 --- a/docs/monitoring/cluster-alerts.asciidoc +++ b/docs/monitoring/cluster-alerts.asciidoc @@ -46,8 +46,18 @@ include::cluster-alerts-license.asciidoc[] ==== Email Notifications To receive email notifications for the Cluster Alerts: -1. Configure an email account as described in -{stack-ov}/actions-email.html#configuring-email[Configuring Email Accounts]. -2. Configure the `xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in `kibana.yml` with your email address. +. Configure an email account as described in +{stack-ov}/actions-email.html#configuring-email[Configuring email accounts]. +. Configure the +`xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in +`kibana.yml` with your email address. ++ +-- +TIP: If you have separate production and monitoring clusters and separate {kib} +instances for those clusters, you must put the +`xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in +the {kib} instance that is associated with the production cluster. + +-- Email notifications are sent only when Cluster Alerts are triggered and resolved. diff --git a/docs/security/authorization/kibana-privileges.asciidoc b/docs/security/authorization/kibana-privileges.asciidoc index 034b3254c96f..4f2493a57b53 100644 --- a/docs/security/authorization/kibana-privileges.asciidoc +++ b/docs/security/authorization/kibana-privileges.asciidoc @@ -36,7 +36,7 @@ PUT /api/security/role/my_kibana_role -------------------------------------------------- - +[[kibana-feature-privileges]] ==== Feature privileges Assigning a feature privilege grants access to a specific feature. diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 615dc98c066b..e47326a1d206 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -21,6 +21,10 @@ xpack.apm.ui.enabled:: Set to `false` to hide the APM plugin {kib} from the menu xpack.apm.ui.transactionGroupBucketSize:: Number of top transaction groups displayed in APM plugin in Kibana. Defaults to `100`. +xpack.apm.ui.maxTraceItems:: Max number of child items displayed when viewing trace details. Defaults to `1000`. + +apm_oss.apmAgentConfigurationIndex:: Index containing agent configuration settings. Defaults to `.apm-agent-configuration`. + apm_oss.indexPattern:: Index pattern is used for integrations with Machine Learning and Kuery Bar. It must match all apm indices. Defaults to `apm-*`. apm_oss.errorIndices:: Matcher for indices containing error documents. Defaults to `apm-*`. diff --git a/docs/settings/code-settings.asciidoc b/docs/settings/code-settings.asciidoc index 57cbaa53f1d6..e01858e6230b 100644 --- a/docs/settings/code-settings.asciidoc +++ b/docs/settings/code-settings.asciidoc @@ -35,11 +35,11 @@ Whitelist of protocols for git clone address. Defaults to `[ 'https', 'git', 'ss `xpack.code.security.enableGitCertCheck`:: Whether enable HTTPS certificate check when clone from HTTPS URL. -`xpack.code.disableIndexScheduler`:: -Whether automatic index update is disabled. Defaults to `true`. - `xpack.code.maxWorkspace`:: Maximal number of workspaces each language server allows to span. Defaults to `5`. `xpack.code.codeNodeUrl`:: URL of the Code node. This config is only needed when multiple Kibana instances are set up as a cluster. Defaults to `` + +`xpack.code.verbose`:: +Set this config to `true` to log all events. Defaults to `false` diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 74a7e4b61170..7330c7e144b6 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -129,6 +129,3 @@ For {es} clusters that are running in containers, this setting changes the statistics. It also adds the calculated Cgroup CPU utilization to the *Node Overview* page instead of the overall operating system's CPU utilization. Defaults to `false`. -+ -[role="screenshot"] -image::images/monitoring-containers.png[Elasticsearch Inside a Container] diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 8434ed469695..2ba1369369a6 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -13,17 +13,18 @@ You do not need to configure any additional settings to use the ==== General security settings `xpack.security.enabled`:: -Set to `true` (default) to enable {security-features}. + -+ -Do not set this to `false`. To disable {security-features} entirely, see -{ref}/security-settings.html[{es} security settings]. + -+ -If set to `false` in `kibana.yml`, the login form, user and role management screens, and -authorization using <> are disabled. + +By default, {kib} automatically detects whether to enable the +{security-features} based on the license and whether {es} {security-features} +are enabled. + +Do not set this to `false`; it disables the login form, user and role management +screens, and authorization using <>. To disable +{security-features} entirely, see +{ref}/security-settings.html[{es} security settings]. + `xpack.security.audit.enabled`:: -Set to `true` to enable audit logging for security events. This is set to `false` by default. -For more details see <>. +Set to `true` to enable audit logging for security events. By default, it is set +to `false`. For more details see <>. [float] [[security-ui-settings]] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 316a4bda65c6..336adea0758a 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -32,7 +32,7 @@ instances of `{nonce}` will be replaced with an automatically generated nonce at load time. We strongly recommend that you keep the default CSP rules that ship with Kibana. -`csp.strict:`:: *Default: `false`* Blocks access to Kibana to any browser that +`csp.strict:`:: *Default: `true`* Blocks access to Kibana to any browser that does not enforce even rudimentary CSP rules. In practice, this will disable support for older, less safe browsers like Internet Explorer. @@ -312,6 +312,8 @@ supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2 setting this to `true` enables unauthenticated users to access the Kibana server status API and status page. +`vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. + `xpack.license_management.enabled`:: *Default: true* Set this value to false to disable the License Management user interface. diff --git a/docs/siem/images/ml-ui.png b/docs/siem/images/ml-ui.png new file mode 100644 index 000000000000..168ff6363186 Binary files /dev/null and b/docs/siem/images/ml-ui.png differ diff --git a/docs/siem/index.asciidoc b/docs/siem/index.asciidoc index 4606f351d6d6..6d8aea1f7fe6 100644 --- a/docs/siem/index.asciidoc +++ b/docs/siem/index.asciidoc @@ -52,3 +52,4 @@ SIEM can ingest and normalize events from ECS-compatible data sources. include::siem-ui.asciidoc[] +include::machine-learning.asciidoc[] diff --git a/docs/siem/machine-learning.asciidoc b/docs/siem/machine-learning.asciidoc new file mode 100644 index 000000000000..dd1016d8550e --- /dev/null +++ b/docs/siem/machine-learning.asciidoc @@ -0,0 +1,16 @@ +[role="xpack"] +[[machine-learning]] +== Anomaly Detection with Machine Learning + +For *https://www.elastic.co/cloud/elasticsearch-service/signup[Free Trial]* +and *https://www.elastic.co/subscriptions[Platinum License]* deployments, +Machine Learning functionality is available throughout the SIEM app. You can +view the details of detected anomalies within the `Anomalies` table widget +shown on the Hosts, Network and associated Details pages, or even narrow to +the specific daterange of an anomaly from the `Max Anomaly Score` details in +the overview of the Host and IP Details pages. Each of these interfaces also +offer the ability to drag and drop details of the anomaly to Timeline, such +as the `Entity` itself, or any of the associated `Influencers`. + +[role="screenshot"] +image::siem/images/ml-ui.png[Machine Learning - Max Anomaly Score] diff --git a/docs/uptime-guide/install.asciidoc b/docs/uptime-guide/install.asciidoc index ffb310913f7c..c8c822022500 100644 --- a/docs/uptime-guide/install.asciidoc +++ b/docs/uptime-guide/install.asciidoc @@ -63,7 +63,7 @@ image::images/uptime-setup.png[Installation instructions on the Uptime page in K * Index patterns tell Kibana which Elasticsearch indices you want to explore. The Uptime UI requires a +heartbeat-{short-version}*+ index pattern. -If you have configured a different index pattern, you can use {ref}/indices-aliases.html[field aliases] to ensure data is recognized by the UI. +If you have configured a different index pattern, you can use {ref}/indices-aliases.html[index aliases] to ensure data is recognized by the UI. After you install and configure Heartbeat, the {kibana-ref}/xpack-uptime.html[Uptime UI] will automatically populate with the Heartbeat monitors. diff --git a/docs/visualize.asciidoc b/docs/visualize.asciidoc index 88e1ccdf8ab6..535d07cedf7e 100644 --- a/docs/visualize.asciidoc +++ b/docs/visualize.asciidoc @@ -140,6 +140,8 @@ Aggregation Execution Order, and You]. include::visualize/saving.asciidoc[] +include::visualize/visualize_rollup_data.asciidoc[] + include::visualize/xychart.asciidoc[] include::visualize/controls.asciidoc[] diff --git a/docs/visualize/timelion.asciidoc b/docs/visualize/timelion.asciidoc index d65ac3f6a2ce..891219a55e5d 100644 --- a/docs/visualize/timelion.asciidoc +++ b/docs/visualize/timelion.asciidoc @@ -1,5 +1,5 @@ [[timelion]] -== Visualizing your data with 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 @@ -20,13 +20,13 @@ In this tutorial, you'll use the time series data from https://www.elastic.co/gu [float] [[time-series-intro]] -== Create time series visualizations +=== 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 +==== Define the functions To start tracking the real-time percentage of CPU, enter the following in the *Timelion Expression* field: @@ -35,12 +35,13 @@ To start tracking the real-time percentage of CPU, enter the following in the *T .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 +==== Compare the data To compare the two data sets, add another series with data from the previous hour, separated by a comma: @@ -51,12 +52,13 @@ To compare the two data sets, add another series with data from the previous hou <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 +==== Add label names To easily distinguish between the two data sets, add the label names: @@ -67,12 +69,13 @@ To easily distinguish between the two data sets, add the label names: <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 title Add a meaningful title: @@ -83,12 +86,13 @@ Add a meaningful title: <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 +==== Change the chart type To differentiate between the current hour data and the last hour data, change the chart type: @@ -99,12 +103,13 @@ To differentiate between the current hour data and the last hour data, change th <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 +==== Change the line colors To make the current hour data stand out, change the line colors: @@ -115,12 +120,13 @@ To make the current hour data stand out, change the line colors: <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 +==== Make adjustments to the legend Change the position and style of the legend: @@ -131,18 +137,19 @@ Change the position and style of the legend: <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 +=== 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 +==== Define the functions To start tracking the inbound and outbound network traffic, enter the following in the *Timelion Expression* field: @@ -151,12 +158,13 @@ To start tracking the inbound and outbound network traffic, enter the following .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 +==== Plot the rate of change Change how the data is displayed so that you can easily monitor the inbound traffic: @@ -167,6 +175,7 @@ Change how the data is displayed so that you can easily monitor the inbound traf <1> `.derivative` plots the change in values over time. +[role="screenshot"] image::images/timelion-math02.png[] {nbsp} @@ -179,12 +188,13 @@ Add a similar calculation for outbound traffic: <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 +==== Change the data metric To make the visualization easier to analyze, change the data metric from bytes to megabytes: @@ -195,12 +205,13 @@ To make the visualization easier to analyze, change the data metric from bytes t <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 Customize and format the visualization using functions: @@ -215,12 +226,13 @@ Customize and format the visualization using functions: <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 +=== 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. @@ -236,7 +248,7 @@ With Timelion conditional logic, you can use the following operator values to co [float] [[conditional-define-functions]] -=== Define the functions +==== Define the functions To chart the maximum value of `system.memory.actual.used.bytes`, enter the following in the *Timelion Expression* field: @@ -245,12 +257,13 @@ To chart the maximum value of `system.memory.actual.used.bytes`, enter the follo .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 +==== Track used memory To track the amount of memory used, create two thresholds: @@ -262,12 +275,13 @@ To track the amount of memory used, create two thresholds: <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 +==== Determine the trend To determine the trend, create a new data series: @@ -278,12 +292,13 @@ To determine the trend, create a new data series: <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 Customize and format the visualization using functions: @@ -298,6 +313,7 @@ Customize and format the visualization using functions: <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} diff --git a/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc index b52066a71837..ff4160d1ac9d 100644 --- a/docs/visualize/tsvb.asciidoc +++ b/docs/visualize/tsvb.asciidoc @@ -1,5 +1,5 @@ [[TSVB]] -== Visualizing your data with 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 @@ -7,6 +7,10 @@ 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. @@ -47,7 +51,7 @@ To create a TSVB visualization, choose the data series you want to display, then [float] [[tsvb-data-series-options]] -=== Configure the data series +==== Configure the data series To create a single metric, add multiple data series with multiple aggregations. @@ -85,7 +89,7 @@ By default, the data series are grouped by everything. [float] [[tsvb-panel-options]] -=== Configure the panel +==== Configure the panel Change the data that you want to display and choose the style options for the panel. @@ -97,7 +101,7 @@ Change the data that you want to display and choose the style options for the pa [float] [[tsvb-add-annotations]] -=== Add annotations +==== Add annotations If you are using the Time Series visualization, add annotation data sources. @@ -107,7 +111,7 @@ If you are using the Time Series visualization, add annotation data sources. [float] [[tsvb-enter-markdown]] -=== Enter Markdown text +==== Enter Markdown text Edit the source for the Markdown visualization. diff --git a/docs/visualize/visualize_rollup_data.asciidoc b/docs/visualize/visualize_rollup_data.asciidoc new file mode 100644 index 000000000000..c2707e2d6710 --- /dev/null +++ b/docs/visualize/visualize_rollup_data.asciidoc @@ -0,0 +1,44 @@ +[role="xpack"] +[[visualize-rollup-data]] +== Using rolled up data in a visualization + +beta[] + +You can visualize your rolled up data in a variety of charts, tables, maps, and +more. Most visualizations support rolled up data, with the exception of +Timelion, TSVB, and Vega visualizations. + +To get started, go to *Management > Kibana > Index patterns.* +If a rollup index is detected in the cluster, *Create index pattern* +includes an item for creating a rollup index pattern. + +[role="screenshot"] +image::images/management_create_rollup_menu.png[Create index pattern menu] + +You can match an index pattern to only rolled up data, or mix both rolled up +and raw data to visualize all data together. An index pattern can match only one +rolled up index, not multiple. There is no restriction on the number of standard +indices that an index pattern can match. When matching multiple indices, +use a comma to separate the names, with no space after the comma. + +Keep the following in mind when creating a visualization from rolled up data: + +* The data in a rollup index only has summarized metrics for specific fields. +You can’t search any other field from the original raw data. +* Data is summarized into time buckets that might be split into sub buckets for +numeric field values or terms. You can ask for a time aggregation that takes +several time buckets and combines them to lower granularity. For example, +if the rollup job was aggregated by hours, you can ask for buckets of days. + +The following visualization of rolled up data shows the date histogram +interval multiple and the limited metrics aggregations. + +[role="screenshot"] +image::images/management_rollups_visualization.png[][Rollups in visualizations] + +Dashboards can have a mixture of rollup visualizations and regular visualizations, +as shown in the following figure. Note that not all queries and filters support rollups. + +[role="screenshot"] +image::images/management_rolled_dashboard.png[][Rollups in dashboards] + diff --git a/kibana.d.ts b/kibana.d.ts index 45cf4405a8ed..e0b20f6fa28a 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -20,8 +20,8 @@ /** * All exports from TS source files (where the implementation is actually done in TS). */ -import * as Public from 'target/types/public'; -import * as Server from 'target/types/server'; +import * as Public from 'src/core/public'; +import * as Server from 'src/core/server'; export { Public, Server }; diff --git a/package.json b/package.json index 2f8a07ab7b29..46793277295a 100644 --- a/package.json +++ b/package.json @@ -104,9 +104,9 @@ "@babel/core": "7.4.5", "@babel/polyfill": "7.4.4", "@babel/register": "7.4.4", - "@elastic/charts": "^7.2.1", + "@elastic/charts": "^8.1.6", "@elastic/datemath": "5.0.2", - "@elastic/eui": "13.0.0", + "@elastic/eui": "13.1.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -141,6 +141,7 @@ "brace": "0.11.1", "cache-loader": "^4.0.1", "chalk": "^2.4.1", + "check-disk-space": "^2.1.0", "color": "1.0.3", "commander": "2.20.0", "compare-versions": "3.4.0", @@ -150,7 +151,6 @@ "d3": "3.5.17", "d3-cloud": "1.2.5", "del": "^4.0.0", - "dragula": "3.7.2", "elasticsearch": "^16.2.0", "elasticsearch-browser": "^16.2.0", "encode-uri-query": "1.0.1", @@ -197,7 +197,6 @@ "moment-timezone": "^0.5.14", "mustache": "2.3.2", "ngreact": "0.5.1", - "no-ui-slider": "1.2.0", "node-fetch": "1.7.3", "opn": "^5.4.0", "oppsy": "^2.0.0", @@ -268,6 +267,7 @@ "@elastic/eslint-config-kibana": "0.15.0", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^4.4.0", + "@kbn/dev-utils": "1.0.0", "@kbn/es": "1.0.0", "@kbn/eslint-import-resolver-kibana": "2.0.0", "@kbn/eslint-plugin-eslint": "1.0.0", @@ -352,7 +352,7 @@ "chance": "1.0.18", "cheerio": "0.22.0", "chokidar": "3.0.1", - "chromedriver": "^75.1.0", + "chromedriver": "^76.0.0", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index b4308dc3f80d..4f0a1fc02adf 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -36,6 +36,7 @@ import { MapOfOptions, MapOfType, MaybeType, + NullableType, NeverType, NumberOptions, NumberType, @@ -100,6 +101,10 @@ function maybe(type: Type): Type { return new MaybeType(type); } +function nullable(type: Type): Type { + return new NullableType(type); +} + function object

(props: P, options?: ObjectTypeOptions

): ObjectType

{ return new ObjectType(props, options); } @@ -191,6 +196,7 @@ export const schema = { literal, mapOf, maybe, + nullable, never, number, object, diff --git a/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap index ba3ac821a97c..fdb172df356a 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap @@ -4,4 +4,6 @@ exports[`fails if null 1`] = `"expected value of type [string] but got [null]"`; exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [string] but got [null]"`; +exports[`validates basic type 1`] = `"expected value of type [string] but got [number]"`; + exports[`validates contained type 1`] = `"value is [foo] but it must have a maximum length of [1]."`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap new file mode 100644 index 000000000000..ae1a34c00d3a --- /dev/null +++ b/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`includes namespace in failure 1`] = `"[foo-namespace]: value is [foo] but it must have a maximum length of [1]."`; + +exports[`validates basic type 1`] = `"expected value of type [string] but got [number]"`; + +exports[`validates contained type 1`] = `"value is [foo] but it must have a maximum length of [1]."`; diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index cfa8cc4b7553..4c25cade2ec6 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -26,6 +26,7 @@ export { ConditionalType, ConditionalTypeValue } from './conditional_type'; export { DurationOptions, DurationType } from './duration_type'; export { LiteralType } from './literal_type'; export { MaybeType } from './maybe_type'; +export { NullableType } from './nullable_type'; export { MapOfOptions, MapOfType } from './map_type'; export { NumberOptions, NumberType } from './number_type'; export { ObjectType, ObjectTypeOptions, Props, TypeOf } from './object_type'; diff --git a/packages/kbn-config-schema/src/types/maybe_type.test.ts b/packages/kbn-config-schema/src/types/maybe_type.test.ts index b29f504c03b3..ecc1d218e186 100644 --- a/packages/kbn-config-schema/src/types/maybe_type.test.ts +++ b/packages/kbn-config-schema/src/types/maybe_type.test.ts @@ -45,6 +45,12 @@ test('validates contained type', () => { expect(() => type.validate('foo')).toThrowErrorMatchingSnapshot(); }); +test('validates basic type', () => { + const type = schema.maybe(schema.string()); + + expect(() => type.validate(666)).toThrowErrorMatchingSnapshot(); +}); + test('fails if null', () => { const type = schema.maybe(schema.string()); expect(() => type.validate(null)).toThrowErrorMatchingSnapshot(); diff --git a/packages/kbn-config-schema/src/types/nullable_type.test.ts b/packages/kbn-config-schema/src/types/nullable_type.test.ts new file mode 100644 index 000000000000..e9d6f3ca2fe3 --- /dev/null +++ b/packages/kbn-config-schema/src/types/nullable_type.test.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 '..'; + +test('returns value if specified', () => { + const type = schema.nullable(schema.string()); + expect(type.validate('test')).toEqual('test'); +}); + +test('returns null if null', () => { + const type = schema.nullable(schema.string()); + expect(type.validate(null)).toEqual(null); +}); + +test('returns null if undefined', () => { + const type = schema.nullable(schema.string()); + expect(type.validate(undefined)).toEqual(null); +}); + +test('returns null even if contained type has a default value', () => { + const type = schema.nullable( + schema.string({ + defaultValue: 'abc', + }) + ); + + expect(type.validate(undefined)).toEqual(null); +}); + +test('validates contained type', () => { + const type = schema.nullable(schema.string({ maxLength: 1 })); + + expect(() => type.validate('foo')).toThrowErrorMatchingSnapshot(); +}); + +test('validates basic type', () => { + const type = schema.nullable(schema.string()); + + expect(() => type.validate(666)).toThrowErrorMatchingSnapshot(); +}); + +test('includes namespace in failure', () => { + const type = schema.nullable(schema.string({ maxLength: 1 })); + + expect(() => type.validate('foo', {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); +}); diff --git a/packages/kbn-config-schema/src/types/nullable_type.ts b/packages/kbn-config-schema/src/types/nullable_type.ts new file mode 100644 index 000000000000..c89f3e44c37c --- /dev/null +++ b/packages/kbn-config-schema/src/types/nullable_type.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Type } from './type'; + +export class NullableType extends Type { + constructor(type: Type) { + super( + type + .getSchema() + .optional() + .allow(null) + .default(null) + ); + } +} diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index 5cd347f9808b..71c7d2e31101 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "scripts": { - "build": "babel src --out-dir target", + "build": "babel src --out-dir target --copy-files", "kbn:bootstrap": "yarn build --quiet", "kbn:watch": "yarn build --watch" }, diff --git a/packages/kbn-dev-utils/src/certs/ca.crt b/packages/kbn-dev-utils/src/certs/ca.crt new file mode 100755 index 000000000000..3e964823c508 --- /dev/null +++ b/packages/kbn-dev-utils/src/certs/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIVAOgxLlE1RMGl2fYgTKDznvDL2vboMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMB4XDTE5MDcxMTE3MzQ0OFoXDTIyMDcxMDE3MzQ0OFowNDEyMDAG +A1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCNImKp/A9l++Ac7U5lvHOA ++fYRb8p7AgdfKBMB0v3bo+bpHjkbkf3vYHjo1xJSg5ls6EPK+Do4owkAgKJdrznI +5/efJOjgA+ylH4rgAfrRIQmiFEWZnAv86vJ+Iq83mfkPELb4dvXCi7AFQkzoM/rY +Lbi97xha5bA2SEmpYp7VhBTM9zWy+q9Tm5odPO8u2n75GpIM2RwipaXlL0ink+06 +/oweQJoivaDgpDOmUXCFPmpV3VCdhUGxDQPyG0upQkF+NbQoei4RmluPEmVz4S7I +TFLWjX7LeZVP63bJkcCgiq6Hm97kDtr9EYlPKhHm7UMWzhNzHbfvySMDzqAJC0KX +AgMBAAGjUzBRMB0GA1UdDgQWBBRKqaaQ/+jT+ipPLJe7qekp1N/zizAfBgNVHSME +GDAWgBRKqaaQ/+jT+ipPLJe7qekp1N/zizAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBCwUAA4IBAQA7Gcq8h8yDXvepfKUAcTTMCBZkI+g3qE1gfRwjW7587CIj +xnrzEqANU+Q1lv7IeQ158HiduDUMZfnvpuNwkf0HkqnRWb57RwfVdCAlAeZmzipq +5ZJWlIW4dbmk57nGLg4fCszedi0uSGytZ2/BUdpWyC0fAM97h7Agtr4xGGKMEL67 +uB55ijt61V62HZ5wWXWNO9m+wfmdnt+YQViQJHtpYz1oOmWhY3dpitZLfWs1sLLD +w3CZOhmWX7+P7+HlCkSBF4swzHOCI3THyX61NbLxju8VkTAjwbZPq4EOnVKnO6kr +RdwQVnzKnqG5fxfSGknNahy0pOhJHZlGLwECRlgF +-----END CERTIFICATE----- diff --git a/packages/kbn-dev-utils/src/certs/elasticsearch.crt b/packages/kbn-dev-utils/src/certs/elasticsearch.crt new file mode 100755 index 000000000000..b30e11e9bbce --- /dev/null +++ b/packages/kbn-dev-utils/src/certs/elasticsearch.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDRDCCAiygAwIBAgIVAI8V1fwvXKykKtp5k0cLpTOtY+DVMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMB4XDTE5MDcxMTE3MzUxOFoXDTIyMDcxMDE3MzUxOFowGDEWMBQG +A1UEAxMNZWxhc3RpY3NlYXJjaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALW+8gV6m6wYmTZmrXzNWKElE+ePkkikCviNfuWonWqxgAoWpAwAx2FvdhP3 +UDFbe38ydJX4oDgXeC25vdIR6z2uqzx+GXSNSybO7luuOUYQOP4Xf5Cj3zzXXMyu +nY1nZTVsChI9jAMz4cZZdUd04f4r4TBNxrFCcVR0uec5RGRXuP8rSQd9AbYFUVYf +jJeLb24asghb2Ku+c2JGvMqPEXFWFGOXFhUoIbRjCJNTDcr1ZXPof3+fO1l6HmhT +QBSqC4IZL8XqANltDT4tCQDD8L9+ckWJD8MP3wPkPUGZId2gLu++hrb9YfiP2upq +N/f3P7l5Fcisw1iwQC4+DGMTyfcCAwEAAaNpMGcwHQYDVR0OBBYEFGuiGk8HLpG2 +MyA24/J+GwxT32ikMB8GA1UdIwQYMBaAFEqpppD/6NP6Kk8sl7up6SnU3/OLMBoG +A1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB +CwUAA4IBAQB8yfY0edAgq2KnJNWyl8NpHNfqtM27+/LR2V8OxVwxV1hc4ZilczLu +CXeqP9uqBVjcck6fvLrjy4LhSG0V05j51UMJ1FjFVTBuhlrDcd3j8848yWrmyz8z +vPYYY2vIN9d1NsBgufULwliBT4UJchsYE8xT5ayAzGHKCTlzHGHMTPzYjwac8nbT +nd2u+6h0OQOJn6K4v+RfXtN4EA8ZUrYxUkqHNS3cFB5sxH7JQGi25XJc5MfxyCwY +YOukxbN85ew861N6oVd+W+nGJu8WOLU88/uvCv+dLhnAlnnIOLqvmrD5m7gFsFO9 +Z7Xz/U1SbNipWy9OLOhqq2Ja59j8p9e5 +-----END CERTIFICATE----- diff --git a/packages/kbn-dev-utils/src/certs/elasticsearch.key b/packages/kbn-dev-utils/src/certs/elasticsearch.key new file mode 100755 index 000000000000..1013ce397124 --- /dev/null +++ b/packages/kbn-dev-utils/src/certs/elasticsearch.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAtb7yBXqbrBiZNmatfM1YoSUT54+SSKQK+I1+5aidarGAChak +DADHYW92E/dQMVt7fzJ0lfigOBd4Lbm90hHrPa6rPH4ZdI1LJs7uW645RhA4/hd/ +kKPfPNdczK6djWdlNWwKEj2MAzPhxll1R3Th/ivhME3GsUJxVHS55zlEZFe4/ytJ +B30BtgVRVh+Ml4tvbhqyCFvYq75zYka8yo8RcVYUY5cWFSghtGMIk1MNyvVlc+h/ +f587WXoeaFNAFKoLghkvxeoA2W0NPi0JAMPwv35yRYkPww/fA+Q9QZkh3aAu776G +tv1h+I/a6mo39/c/uXkVyKzDWLBALj4MYxPJ9wIDAQABAoIBAQCb1ggrjn/gxo7I +yK3FL0XplqNEkCR8SLxndtvyC+w+Schh3hv3dst+zlXOtOZ8C9cOr7KrzS2EKwuP +GY6bi2XL0/NbwTwOZgCkXBahYfgWDV7w8DEfUoPd5UPa9XZ+gsOTVPolvcRKErhq +nNYk2SHWEMXb5zSRVUlbg2LL0pzD88bIuKJX+FwPvWcQc2P4OdVTq77iedcl82zZ +6PqTNqKMep7/odLQeBfX7OapOAviVnPYHe0TA114COOimR/pK8IA1OJymX5rgU7O +Wh+uNBSxdHsTTYTkAvw8Bt5Q8n1WCpQwZoYU3xWuSlu7eJ7kcgdFOu9r9GjSXysT +UYCd8s0BAoGBAPXPpCDRxjqF3/ToZ5x5dorKxxJyrmldzMJaUjqOv7y6kezbdBql +n7p3AJ5UfYUW/N6pgQXaWF4MPSyj7ItHhwHjL+v0Manmi5gq8oA30fplhjUlPre7 +Lx4v7SEmH739EHrkZ2ClIQwY3wKuN8mZKgw6RseFgphczDmhHCqEbjW3AoGBAL1H +fkl0RNdZ3nZg0u7MUVk8ytnqBsp7bNFhEs0zUl7ghu3NLaPt8qhirG638oMSCxqH +FPeM3/DryokQAym+UHYNMwiBziEUB2CKMMj7S5YFFWIldCxFeImCO2EP+y3hmbTZ +yjsznNrDzQtErZGP+JTRZcy9xF0oAfVt0G/O1Q3BAoGAa8bqINW5g6l1Q82uuEXt +evdkB6uu21YcVE8D5Nb4LMjk+KRUKObbvQc2hzVmf7dPklVh0+4jdsEJBYyuR3dK +M8KoHV3JdMQ4CrUx9JQFBjQDf0PgVvDEvQiogTNVEZlm42tIBHECp2o0RdmbblIw +xIG8zPi2BRYTGWWRkvbT18sCgYA+c/B/XBW62LRGavwuPsw4nY5xCH7lIIRvMZB6 +lIyBMaRToneEt2ZxmN08SwWBqdpwDlIkvB7H54UUZGwmwdzaltBX5jyVPX6RpAck +yYXPIi5EDAeg8+sptAbTp+pA4UdOHO5VSlpe9GwbY7XBabejotPsElFQS3sZ9/nm +amByAQKBgQCJWghllys1qk76/6PmeVjwjaK9n8o+94LWhqODXlACmDRyse5dwpYb +BIsMMZrNu1YsqDXlWpU7xNa6A8j4oa+EPnm/01PjdueAvMB/oE1woawM5tSsd8NQ +zeQPDhxjDxzaO5l4oJLZg6FT7iQAprhYZjgb8m1vz0D2Xid0A3Kgpw== +-----END RSA PRIVATE KEY----- diff --git a/packages/kbn-dev-utils/src/certs/index.js b/packages/kbn-dev-utils/src/certs/index.js new file mode 100644 index 000000000000..97292eec3512 --- /dev/null +++ b/packages/kbn-dev-utils/src/certs/index.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { resolve } from 'path'; + +export const CA_CERT_PATH = resolve(__dirname, 'ca.crt'); +export const ES_KEY_PATH = resolve(__dirname, 'elasticsearch.key'); +export const ES_CERT_PATH = resolve(__dirname, 'elasticsearch.crt'); diff --git a/packages/kbn-dev-utils/src/index.js b/packages/kbn-dev-utils/src/index.js index 82492e568f4b..cba24575bd39 100644 --- a/packages/kbn-dev-utils/src/index.js +++ b/packages/kbn-dev-utils/src/index.js @@ -20,3 +20,4 @@ export { withProcRunner } from './proc_runner'; export { ToolingLog, ToolingLogTextWriter, pickLevelFromFlags } from './tooling_log'; export { createAbsolutePathSerializer } from './serializers'; +export { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from './certs'; diff --git a/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js b/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js index 5c27a204ef26..fde3d063caaa 100644 --- a/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js +++ b/packages/kbn-es-query/src/es_query/__tests__/build_es_query.js @@ -60,10 +60,10 @@ describe('build query', function () { bool: { must: [ decorateQuery(luceneStringToDsl('bar:baz'), config.queryStringOptions), - { match_all: {} }, ], filter: [ toElasticsearchQuery(fromKueryExpression('extension:jpg'), indexPattern), + { match_all: {} }, ], should: [], must_not: [], @@ -90,9 +90,8 @@ describe('build query', function () { bool: { must: [ decorateQuery(luceneStringToDsl('extension:jpg'), config.queryStringOptions), - { match_all: {} }, ], - filter: [], + filter: [{ match_all: {} }], should: [], must_not: [], } @@ -122,9 +121,11 @@ describe('build query', function () { bool: { must: [ decorateQuery(luceneStringToDsl('@timestamp:"2019-03-23T13:18:00"'), config.queryStringOptions, config.dateFormatTZ), + ], + filter: [ + toElasticsearchQuery(fromKueryExpression('@timestamp:"2019-03-23T13:18:00"'), indexPattern, config), { match_all: {} } ], - filter: [toElasticsearchQuery(fromKueryExpression('@timestamp:"2019-03-23T13:18:00"'), indexPattern, config)], should: [], must_not: [], } diff --git a/packages/kbn-es-query/src/es_query/__tests__/from_filters.js b/packages/kbn-es-query/src/es_query/__tests__/from_filters.js index 53016f33dc6c..59e5f4d6faf8 100644 --- a/packages/kbn-es-query/src/es_query/__tests__/from_filters.js +++ b/packages/kbn-es-query/src/es_query/__tests__/from_filters.js @@ -52,7 +52,7 @@ describe('build query', function () { const result = buildQueryFromFilters(filters); - expect(result.must).to.eql(expectedESQueries); + expect(result.filter).to.eql(expectedESQueries); }); it('should place negated filters in the must_not clause', function () { @@ -86,7 +86,7 @@ describe('build query', function () { const result = buildQueryFromFilters(filters); - expect(result.must).to.eql(expectedESQueries); + expect(result.filter).to.eql(expectedESQueries); }); it('should migrate deprecated match syntax', function () { @@ -105,7 +105,7 @@ describe('build query', function () { const result = buildQueryFromFilters(filters); - expect(result.must).to.eql(expectedESQueries); + expect(result.filter).to.eql(expectedESQueries); }); it('should not add query:queryString:options to query_string filters', function () { @@ -119,7 +119,7 @@ describe('build query', function () { const result = buildQueryFromFilters(filters); - expect(result.must).to.eql(expectedESQueries); + expect(result.filter).to.eql(expectedESQueries); }); }); }); diff --git a/packages/kbn-es-query/src/es_query/from_filters.js b/packages/kbn-es-query/src/es_query/from_filters.js index 07f3211b3fc5..b8193b7469a2 100644 --- a/packages/kbn-es-query/src/es_query/from_filters.js +++ b/packages/kbn-es-query/src/es_query/from_filters.js @@ -61,7 +61,8 @@ const cleanFilter = function (filter) { export function buildQueryFromFilters(filters = [], indexPattern, ignoreFilterIfFieldNotInIndex) { return { - must: filters + must: [], + filter: filters .filter(filterNegate(false)) .filter(filter => !ignoreFilterIfFieldNotInIndex || filterMatchesIndex(filter, indexPattern)) .map(translateToQuery) @@ -69,7 +70,6 @@ export function buildQueryFromFilters(filters = [], indexPattern, ignoreFilterIf .map(filter => { return migrateFilter(filter, indexPattern); }), - filter: [], should: [], must_not: filters .filter(filterNegate(true)) diff --git a/packages/kbn-es-query/src/filters/index.d.ts b/packages/kbn-es-query/src/filters/index.d.ts index c46a767e38ea..39f30fa6e7df 100644 --- a/packages/kbn-es-query/src/filters/index.d.ts +++ b/packages/kbn-es-query/src/filters/index.d.ts @@ -17,12 +17,16 @@ * under the License. */ -import { Field, IndexPattern } from 'ui/index_patterns'; import { CustomFilter, ExistsFilter, PhraseFilter, PhrasesFilter, RangeFilter } from './lib'; import { RangeFilterParams } from './lib/range_filter'; export * from './lib'; +// We can't import the real types from the data plugin, so need to either duplicate +// them here or figure out another solution, perhaps housing them in this package +type Field = any; +type IndexPattern = any; + export function buildExistsFilter(field: Field, indexPattern: IndexPattern): ExistsFilter; export function buildPhraseFilter( diff --git a/packages/kbn-es-query/src/kuery/ast/ast.d.ts b/packages/kbn-es-query/src/kuery/ast/ast.d.ts index 484abac809bc..915c024f2ab4 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.d.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.d.ts @@ -21,8 +21,6 @@ * WARNING: these typings are incomplete */ -import { StaticIndexPattern } from 'ui/index_patterns'; - export type KueryNode = any; export interface KueryParseOptions { @@ -46,6 +44,6 @@ export function fromKueryExpression( parseOptions?: KueryParseOptions ): KueryNode; -export function toElasticsearchQuery(node: KueryNode, indexPattern: StaticIndexPattern): JsonObject; +export function toElasticsearchQuery(node: KueryNode, indexPattern: any): JsonObject; export function doesKueryExpressionHaveLuceneSyntaxError(expression: string): boolean; diff --git a/packages/kbn-es/src/cli_commands/archive.js b/packages/kbn-es/src/cli_commands/archive.js index 273cdea87c67..a3197d959fc8 100644 --- a/packages/kbn-es/src/cli_commands/archive.js +++ b/packages/kbn-es/src/cli_commands/archive.js @@ -36,6 +36,7 @@ exports.help = (defaults = {}) => { --install-path Installation path, defaults to 'source' within base-path --password Sets password for elastic user [default: ${password}] --password.[user] Sets password for native realm user [default: ${password}] + --ssl Sets up SSL on Elasticsearch -E Additional key=value settings to pass to Elasticsearch Example: @@ -56,7 +57,7 @@ exports.run = async (defaults = {}) => { default: defaults, }); - const cluster = new Cluster(); + const cluster = new Cluster({ ssl: options.ssl }); const [, path] = options._; if (!path || !path.endsWith('tar.gz')) { diff --git a/packages/kbn-es/src/cli_commands/snapshot.js b/packages/kbn-es/src/cli_commands/snapshot.js index bc8aee7696a1..3139cb18793f 100644 --- a/packages/kbn-es/src/cli_commands/snapshot.js +++ b/packages/kbn-es/src/cli_commands/snapshot.js @@ -38,6 +38,7 @@ exports.help = (defaults = {}) => { --password.[user] Sets password for native realm user [default: ${password}] -E Additional key=value settings to pass to Elasticsearch --download-only Download the snapshot but don't actually start it + --ssl Sets up SSL on Elasticsearch Example: @@ -62,7 +63,7 @@ exports.run = async (defaults = {}) => { default: defaults, }); - const cluster = new Cluster(); + const cluster = new Cluster({ ssl: options.ssl }); if (options['download-only']) { await cluster.downloadSnapshot(options); } else { diff --git a/packages/kbn-es/src/cli_commands/source.js b/packages/kbn-es/src/cli_commands/source.js index 3065ca887bff..5ea95add4ef5 100644 --- a/packages/kbn-es/src/cli_commands/source.js +++ b/packages/kbn-es/src/cli_commands/source.js @@ -36,6 +36,7 @@ exports.help = (defaults = {}) => { --data-archive Path to zip or tarball containing an ES data directory to seed the cluster with. --password Sets password for elastic user [default: ${password}] --password.[user] Sets password for native realm user [default: ${password}] + --ssl Sets up SSL on Elasticsearch -E Additional key=value settings to pass to Elasticsearch Example: @@ -58,7 +59,7 @@ exports.run = async (defaults = {}) => { default: defaults, }); - const cluster = new Cluster(); + const cluster = new Cluster({ ssl: options.ssl }); const { installPath } = await cluster.installSource(options); if (options.dataArchive) { diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 3d2b9956a64c..2c81035d5c25 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -17,6 +17,8 @@ * under the License. */ +const fs = require('fs'); +const util = require('util'); const execa = require('execa'); const chalk = require('chalk'); const path = require('path'); @@ -32,6 +34,9 @@ const { const { createCliError } = require('./errors'); const { promisify } = require('util'); const treeKillAsync = promisify(require('tree-kill')); +const { parseSettings, SettingsFilter } = require('./settings'); +const { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils'); +const readFile = util.promisify(fs.readFile); // listen to data on stream until map returns anything but undefined const first = (stream, map) => @@ -47,8 +52,10 @@ const first = (stream, map) => }); exports.Cluster = class Cluster { - constructor(log = defaultLog) { + constructor({ log = defaultLog, ssl = false } = {}) { this._log = log; + this._ssl = ssl; + this._caCertPromise = ssl ? readFile(CA_CERT_PATH) : undefined; } /** @@ -250,9 +257,21 @@ exports.Cluster = class Cluster { this._log.info(chalk.bold('Starting')); this._log.indent(4); - const args = extractConfigFiles(options.esArgs || [], installPath, { - log: this._log, - }).reduce((acc, cur) => acc.concat(['-E', cur]), []); + // Add to esArgs if ssl is enabled + const esArgs = [].concat(options.esArgs || []); + if (this._ssl) { + esArgs.push('xpack.security.http.ssl.enabled=true'); + esArgs.push(`xpack.security.http.ssl.key=${ES_KEY_PATH}`); + esArgs.push(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`); + esArgs.push(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`); + } + + const args = parseSettings(extractConfigFiles(esArgs, installPath, { log: this._log }), { + filter: SettingsFilter.NonSecureOnly, + }).reduce( + (acc, [settingName, settingValue]) => acc.concat(['-E', `${settingName}=${settingValue}`]), + [] + ); this._log.debug('%s %s', ES_BIN, args.join(' ')); @@ -277,7 +296,14 @@ exports.Cluster = class Cluster { // once the http port is available setup the native realm this._nativeRealmSetup = httpPort.then(async port => { - const nativeRealm = new NativeRealm(options.password, port, this._log); + const caCert = await this._caCertPromise; + const nativeRealm = new NativeRealm({ + port, + caCert, + log: this._log, + elasticPassword: options.password, + ssl: this._ssl, + }); await nativeRealm.setPasswords(options); }); diff --git a/packages/kbn-es/src/install/archive.js b/packages/kbn-es/src/install/archive.js index df4a8502e434..ba675ed6ac20 100644 --- a/packages/kbn-es/src/install/archive.js +++ b/packages/kbn-es/src/install/archive.js @@ -26,6 +26,7 @@ const url = require('url'); const { log: defaultLog, decompress } = require('../utils'); const { BASE_PATH, ES_CONFIG, ES_KEYSTORE_BIN } = require('../paths'); const { Artifact } = require('../artifact'); +const { parseSettings, SettingsFilter } = require('../settings'); /** * Extracts an ES archive and optionally installs plugins @@ -45,6 +46,7 @@ exports.installArchive = async function installArchive(archive, options = {}) { installPath = path.resolve(basePath, path.basename(archive, '.tar.gz')), log = defaultLog, bundledJDK = false, + esArgs = [], } = options; let dest = archive; @@ -69,7 +71,10 @@ exports.installArchive = async function installArchive(archive, options = {}) { await appendToConfig(installPath, 'xpack.security.enabled', 'true'); await appendToConfig(installPath, 'xpack.license.self_generated.type', license); - await configureKeystore(installPath, password, log, bundledJDK); + await configureKeystore(installPath, log, bundledJDK, [ + ['bootstrap.password', password], + ...parseSettings(esArgs, { filter: SettingsFilter.SecureOnly }), + ]); } return { installPath }; @@ -90,21 +95,33 @@ async function appendToConfig(installPath, key, value) { * Creates and configures Keystore * * @param {String} installPath - * @param {String} password * @param {ToolingLog} log + * @param {boolean} bundledJDK + * @param {Array<[string, string]>} secureSettings List of custom Elasticsearch secure settings to + * add into the keystore. */ -async function configureKeystore(installPath, password, log = defaultLog, bundledJDK = false) { - log.info('setting bootstrap password to %s', chalk.bold(password)); - +async function configureKeystore( + installPath, + log = defaultLog, + bundledJDK = false, + secureSettings +) { const env = {}; if (bundledJDK) { env.JAVA_HOME = ''; } await execa(ES_KEYSTORE_BIN, ['create'], { cwd: installPath, env }); - await execa(ES_KEYSTORE_BIN, ['add', 'bootstrap.password', '-x'], { - input: password, - cwd: installPath, - env, - }); + for (const [secureSettingName, secureSettingValue] of secureSettings) { + log.info( + `setting secure setting %s to %s`, + chalk.bold(secureSettingName), + chalk.bold(secureSettingValue) + ); + await execa(ES_KEYSTORE_BIN, ['add', secureSettingName, '-x'], { + input: secureSettingValue, + cwd: installPath, + env, + }); + } } diff --git a/packages/kbn-es/src/install/snapshot.js b/packages/kbn-es/src/install/snapshot.js index bfe2c8833ec8..57aa276de09c 100644 --- a/packages/kbn-es/src/install/snapshot.js +++ b/packages/kbn-es/src/install/snapshot.js @@ -73,6 +73,7 @@ exports.installSnapshot = async function installSnapshot({ installPath = path.resolve(basePath, version), log = defaultLog, bundledJDK = true, + esArgs, }) { const { downloadPath } = await exports.downloadSnapshot({ license, @@ -89,5 +90,6 @@ exports.installSnapshot = async function installSnapshot({ installPath, log, bundledJDK, + esArgs, }); }; diff --git a/packages/kbn-es/src/install/source.js b/packages/kbn-es/src/install/source.js index e8ef43a897da..e78e9f1ff4b2 100644 --- a/packages/kbn-es/src/install/source.js +++ b/packages/kbn-es/src/install/source.js @@ -45,6 +45,7 @@ exports.installSource = async function installSource({ basePath = BASE_PATH, installPath = path.resolve(basePath, 'source'), log = defaultLog, + esArgs, }) { log.info('source path: %s', chalk.bold(sourcePath)); log.info('install path: %s', chalk.bold(installPath)); @@ -70,6 +71,7 @@ exports.installSource = async function installSource({ basePath, installPath, log, + esArgs, }); }; diff --git a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js index 6c49371fd7d4..d3181a748ffb 100644 --- a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js +++ b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js @@ -19,9 +19,11 @@ * under the License. */ -const { createServer } = require('http'); +const fs = require('fs'); const { format: formatUrl } = require('url'); -const { exitCode, start } = JSON.parse(process.argv[2]); +const { exitCode, start, ssl } = JSON.parse(process.argv[2]); +const { createServer } = ssl ? require('https') : require('http'); +const { ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils'); process.exitCode = exitCode; @@ -30,27 +32,33 @@ if (!start) { } let serverUrl; -const server = createServer((req, res) => { - const url = new URL(req.url, serverUrl); - const send = (code, body) => { - res.writeHead(code, { 'content-type': 'application/json' }); - res.end(JSON.stringify(body)); - }; +const server = createServer( + { + key: ssl ? fs.readFileSync(ES_KEY_PATH) : undefined, + cert: ssl ? fs.readFileSync(ES_CERT_PATH) : undefined, + }, + (req, res) => { + const url = new URL(req.url, serverUrl); + const send = (code, body) => { + res.writeHead(code, { 'content-type': 'application/json' }); + res.end(JSON.stringify(body)); + }; - if (url.pathname === '/_xpack') { - return send(400, { + if (url.pathname === '/_xpack') { + return send(400, { + error: { + reason: 'foo bar', + }, + }); + } + + return send(404, { error: { - reason: 'foo bar', + reason: 'not found', }, }); } - - return send(404, { - error: { - reason: 'not found', - }, - }); -}); +); // setup server auto close after 1 second of silence let serverCloseTimer; diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js index e74f94d92bee..dd570e27e328 100644 --- a/packages/kbn-es/src/integration_tests/cluster.test.js +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -17,10 +17,11 @@ * under the License. */ -const { ToolingLog } = require('@kbn/dev-utils'); +const { ToolingLog, CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils'); const execa = require('execa'); const { Cluster } = require('../cluster'); const { installSource, installSnapshot, installArchive } = require('../install'); +const { extractConfigFiles } = require('../utils/extract_config_files'); jest.mock('../install', () => ({ installSource: jest.fn(), @@ -29,6 +30,9 @@ jest.mock('../install', () => ({ })); jest.mock('execa', () => jest.fn()); +jest.mock('../utils/extract_config_files', () => ({ + extractConfigFiles: jest.fn(), +})); const log = new ToolingLog(); @@ -63,6 +67,7 @@ function mockEsBin({ exitCode, start }) { JSON.stringify({ exitCode, start, + ssl: args.includes('xpack.security.http.ssl.enabled=true'), }), ], options @@ -72,6 +77,7 @@ function mockEsBin({ exitCode, start }) { beforeEach(() => { jest.resetAllMocks(); + extractConfigFiles.mockImplementation(config => config); }); describe('#installSource()', () => { @@ -86,7 +92,7 @@ describe('#installSource()', () => { }) ); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); const promise = cluster.installSource(); await ensureNoResolve(promise); resolveInstallSource(); @@ -97,7 +103,7 @@ describe('#installSource()', () => { it('passes through all options+log to installSource()', async () => { installSource.mockResolvedValue({}); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.installSource({ foo: 'bar' }); expect(installSource).toHaveBeenCalledTimes(1); expect(installSource).toHaveBeenCalledWith({ @@ -108,7 +114,7 @@ describe('#installSource()', () => { it('rejects if installSource() rejects', async () => { installSource.mockRejectedValue(new Error('foo')); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await expect(cluster.installSource()).rejects.toThrowError('foo'); }); }); @@ -125,7 +131,7 @@ describe('#installSnapshot()', () => { }) ); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); const promise = cluster.installSnapshot(); await ensureNoResolve(promise); resolveInstallSnapshot(); @@ -136,7 +142,7 @@ describe('#installSnapshot()', () => { it('passes through all options+log to installSnapshot()', async () => { installSnapshot.mockResolvedValue({}); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.installSnapshot({ foo: 'bar' }); expect(installSnapshot).toHaveBeenCalledTimes(1); expect(installSnapshot).toHaveBeenCalledWith({ @@ -147,7 +153,7 @@ describe('#installSnapshot()', () => { it('rejects if installSnapshot() rejects', async () => { installSnapshot.mockRejectedValue(new Error('foo')); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await expect(cluster.installSnapshot()).rejects.toThrowError('foo'); }); }); @@ -164,7 +170,7 @@ describe('#installArchive(path)', () => { }) ); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); const promise = cluster.installArchive(); await ensureNoResolve(promise); resolveInstallArchive(); @@ -175,7 +181,7 @@ describe('#installArchive(path)', () => { it('passes through path and all options+log to installArchive()', async () => { installArchive.mockResolvedValue({}); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.installArchive('path', { foo: 'bar' }); expect(installArchive).toHaveBeenCalledTimes(1); expect(installArchive).toHaveBeenCalledWith('path', { @@ -186,7 +192,7 @@ describe('#installArchive(path)', () => { it('rejects if installArchive() rejects', async () => { installArchive.mockRejectedValue(new Error('foo')); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await expect(cluster.installArchive()).rejects.toThrowError('foo'); }); }); @@ -195,37 +201,37 @@ describe('#start(installPath)', () => { it('rejects when bin/elasticsearch exists with 0 before starting', async () => { mockEsBin({ exitCode: 0, start: false }); - await expect(new Cluster(log).start()).rejects.toThrowError('ES exited without starting'); + await expect(new Cluster({ log }).start()).rejects.toThrowError('ES exited without starting'); }); it('rejects when bin/elasticsearch exists with 143 before starting', async () => { mockEsBin({ exitCode: 143, start: false }); - await expect(new Cluster(log).start()).rejects.toThrowError('ES exited without starting'); + await expect(new Cluster({ log }).start()).rejects.toThrowError('ES exited without starting'); }); it('rejects when bin/elasticsearch exists with 130 before starting', async () => { mockEsBin({ exitCode: 130, start: false }); - await expect(new Cluster(log).start()).rejects.toThrowError('ES exited without starting'); + await expect(new Cluster({ log }).start()).rejects.toThrowError('ES exited without starting'); }); it('rejects when bin/elasticsearch exists with 1 before starting', async () => { mockEsBin({ exitCode: 1, start: false }); - await expect(new Cluster(log).start()).rejects.toThrowError('ES exited with code 1'); + await expect(new Cluster({ log }).start()).rejects.toThrowError('ES exited with code 1'); }); it('resolves when bin/elasticsearch logs "started"', async () => { mockEsBin({ start: true }); - await new Cluster(log).start(); + await new Cluster({ log }).start(); }); it('rejects if #start() was called previously', async () => { mockEsBin({ start: true }); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.start(); await expect(cluster.start()).rejects.toThrowError('ES has already been started'); }); @@ -233,41 +239,66 @@ describe('#start(installPath)', () => { it('rejects if #run() was called previously', async () => { mockEsBin({ start: true }); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.run(); await expect(cluster.start()).rejects.toThrowError('ES has already been started'); }); + + it('sets up SSL when enabled', async () => { + mockEsBin({ start: true, ssl: true }); + + const cluster = new Cluster({ log, ssl: true }); + await cluster.start(); + + const config = extractConfigFiles.mock.calls[0][0]; + expect(config).toContain('xpack.security.http.ssl.enabled=true'); + expect(config).toContain(`xpack.security.http.ssl.key=${ES_KEY_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`); + }); + + it(`doesn't setup SSL when disabled`, async () => { + mockEsBin({ start: true }); + + extractConfigFiles.mockReturnValueOnce([]); + + const cluster = new Cluster({ log, ssl: false }); + await cluster.start(); + + const config = extractConfigFiles.mock.calls[0][0]; + expect(config).toHaveLength(0); + }); }); describe('#run()', () => { it('resolves when bin/elasticsearch exists with 0', async () => { mockEsBin({ exitCode: 0 }); - await new Cluster(log).run(); + await new Cluster({ log }).run(); }); it('resolves when bin/elasticsearch exists with 143', async () => { mockEsBin({ exitCode: 143 }); - await new Cluster(log).run(); + await new Cluster({ log }).run(); }); it('resolves when bin/elasticsearch exists with 130', async () => { mockEsBin({ exitCode: 130 }); - await new Cluster(log).run(); + await new Cluster({ log }).run(); }); it('rejects when bin/elasticsearch exists with 1', async () => { mockEsBin({ exitCode: 1 }); - await expect(new Cluster(log).run()).rejects.toThrowError('ES exited with code 1'); + await expect(new Cluster({ log }).run()).rejects.toThrowError('ES exited with code 1'); }); it('rejects if #start() was called previously', async () => { mockEsBin({ exitCode: 0, start: true }); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.start(); await expect(cluster.run()).rejects.toThrowError('ES has already been started'); }); @@ -275,22 +306,47 @@ describe('#run()', () => { it('rejects if #run() was called previously', async () => { mockEsBin({ exitCode: 0 }); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.run(); await expect(cluster.run()).rejects.toThrowError('ES has already been started'); }); + + it('sets up SSL when enabled', async () => { + mockEsBin({ start: true, ssl: true }); + + const cluster = new Cluster({ log, ssl: true }); + await cluster.run(); + + const config = extractConfigFiles.mock.calls[0][0]; + expect(config).toContain('xpack.security.http.ssl.enabled=true'); + expect(config).toContain(`xpack.security.http.ssl.key=${ES_KEY_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`); + }); + + it(`doesn't setup SSL when disabled`, async () => { + mockEsBin({ start: true }); + + extractConfigFiles.mockReturnValueOnce([]); + + const cluster = new Cluster({ log, ssl: false }); + await cluster.run(); + + const config = extractConfigFiles.mock.calls[0][0]; + expect(config).toHaveLength(0); + }); }); describe('#stop()', () => { it('rejects if #run() or #start() was not called', async () => { - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await expect(cluster.stop()).rejects.toThrowError('ES has not been started'); }); it('resolves when ES exits with 0', async () => { mockEsBin({ exitCode: 0, start: true }); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.start(); await cluster.stop(); }); @@ -298,7 +354,7 @@ describe('#stop()', () => { it('resolves when ES exits with 143', async () => { mockEsBin({ exitCode: 143, start: true }); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.start(); await cluster.stop(); }); @@ -306,7 +362,7 @@ describe('#stop()', () => { it('resolves when ES exits with 130', async () => { mockEsBin({ exitCode: 130, start: true }); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await cluster.start(); await cluster.stop(); }); @@ -314,7 +370,7 @@ describe('#stop()', () => { it('rejects when ES exits with 1', async () => { mockEsBin({ exitCode: 1, start: true }); - const cluster = new Cluster(log); + const cluster = new Cluster({ log }); await expect(cluster.run()).rejects.toThrowError('ES exited with code 1'); await expect(cluster.stop()).rejects.toThrowError('ES exited with code 1'); }); diff --git a/packages/kbn-es/src/settings.test.ts b/packages/kbn-es/src/settings.test.ts new file mode 100644 index 000000000000..0a6aa4a97d76 --- /dev/null +++ b/packages/kbn-es/src/settings.test.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { parseSettings, SettingsFilter } from './settings'; + +const mockSettings = [ + 'abc.def=1', + 'xpack.security.authc.realms.oidc.oidc1.rp.client_secret=secret', + 'xpack.security.authc.realms.oidc.oidc1.rp.client_id=client id', + 'discovery.type=single-node', +]; + +test('`parseSettings` parses and returns all settings by default', () => { + expect(parseSettings(mockSettings)).toEqual([ + ['abc.def', '1'], + ['xpack.security.authc.realms.oidc.oidc1.rp.client_secret', 'secret'], + ['xpack.security.authc.realms.oidc.oidc1.rp.client_id', 'client id'], + ['discovery.type', 'single-node'], + ]); +}); + +test('`parseSettings` parses and returns all settings with `SettingsFilter.All` filter', () => { + expect(parseSettings(mockSettings, { filter: SettingsFilter.All })).toEqual([ + ['abc.def', '1'], + ['xpack.security.authc.realms.oidc.oidc1.rp.client_secret', 'secret'], + ['xpack.security.authc.realms.oidc.oidc1.rp.client_id', 'client id'], + ['discovery.type', 'single-node'], + ]); +}); + +test('`parseSettings` parses and returns only secure settings with `SettingsFilter.SecureOnly` filter', () => { + expect(parseSettings(mockSettings, { filter: SettingsFilter.SecureOnly })).toEqual([ + ['xpack.security.authc.realms.oidc.oidc1.rp.client_secret', 'secret'], + ]); +}); + +test('`parseSettings` parses and returns only non-secure settings with `SettingsFilter.NonSecureOnly` filter', () => { + expect(parseSettings(mockSettings, { filter: SettingsFilter.NonSecureOnly })).toEqual([ + ['abc.def', '1'], + ['xpack.security.authc.realms.oidc.oidc1.rp.client_id', 'client id'], + ['discovery.type', 'single-node'], + ]); +}); diff --git a/packages/kbn-es/src/settings.ts b/packages/kbn-es/src/settings.ts new file mode 100644 index 000000000000..58eedff207b4 --- /dev/null +++ b/packages/kbn-es/src/settings.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * List of the patterns for the settings names that are supposed to be secure and stored in the keystore. + */ +const SECURE_SETTINGS_LIST = [ + /^xpack\.security\.authc\.realms\.oidc\.[a-zA-Z0-9_]+\.rp\.client_secret$/, +]; + +function isSecureSetting(settingName: string) { + return SECURE_SETTINGS_LIST.some(secureSettingNameRegex => + secureSettingNameRegex.test(settingName) + ); +} + +export enum SettingsFilter { + All = 'all', + SecureOnly = 'secure-only', + NonSecureOnly = 'non-secure-only', +} + +/** + * Accepts an array of `esSettingName=esSettingValue` strings and parses them into an array of + * [esSettingName, esSettingValue] tuples optionally filter out secure or non-secure settings. + * @param rawSettingNameValuePairs Array of strings to parse + * @param [filter] Optional settings filter. + */ +export function parseSettings( + rawSettingNameValuePairs: string[], + { filter }: { filter: SettingsFilter } = { filter: SettingsFilter.All } +) { + const settings: Array<[string, string]> = []; + for (const rawSettingNameValuePair of rawSettingNameValuePairs) { + const [settingName, settingValue] = rawSettingNameValuePair.split('='); + + const includeSetting = + filter === SettingsFilter.All || + (filter === SettingsFilter.SecureOnly && isSecureSetting(settingName)) || + (filter === SettingsFilter.NonSecureOnly && !isSecureSetting(settingName)); + if (includeSetting) { + settings.push([settingName, settingValue]); + } + } + + return settings; +} diff --git a/packages/kbn-es/src/utils/extract_config_files.js b/packages/kbn-es/src/utils/extract_config_files.js index 1ee4ee887fd3..7ad75dcbbf34 100644 --- a/packages/kbn-es/src/utils/extract_config_files.js +++ b/packages/kbn-es/src/utils/extract_config_files.js @@ -55,7 +55,7 @@ exports.extractConfigFiles = function extractConfigFiles(config, dest, options = }; function isFile(dest = '') { - return path.isAbsolute(dest) && path.extname(dest).length > 0; + return path.isAbsolute(dest) && path.extname(dest).length > 0 && fs.existsSync(dest); } function copyFileSync(src, dest) { diff --git a/packages/kbn-es/src/utils/extract_config_files.test.js b/packages/kbn-es/src/utils/extract_config_files.test.js index 0c417536878d..4d47f08cb134 100644 --- a/packages/kbn-es/src/utils/extract_config_files.test.js +++ b/packages/kbn-es/src/utils/extract_config_files.test.js @@ -47,13 +47,29 @@ test('copies file', () => { expect(fs.writeFileSync.mock.calls[0][0]).toEqual('/es/config/foo.yml'); }); +test('ignores file which does not exist', () => { + fs.existsSync = () => false; + extractConfigFiles(['path=/data/foo.yml'], '/es'); + + expect(fs.readFileSync).not.toHaveBeenCalled(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); +}); + test('ignores non-paths', () => { const config = extractConfigFiles(['foo=bar', 'foo.bar=baz'], '/es'); expect(config).toEqual(['foo=bar', 'foo.bar=baz']); }); +test('keeps regular expressions intact', () => { + fs.existsSync = () => false; + const config = extractConfigFiles(['foo=bar', 'foo.bar=/https?://127.0.0.1(:[0-9]+)?/'], '/es'); + + expect(config).toEqual(['foo=bar', 'foo.bar=/https?://127.0.0.1(:[0-9]+)?/']); +}); + test('ignores directories', () => { + fs.existsSync = () => true; const config = extractConfigFiles(['path=/data/foo.yml', 'foo.bar=/data/bar'], '/es'); expect(config).toEqual(['path=foo.yml', 'foo.bar=/data/bar']); diff --git a/packages/kbn-es/src/utils/native_realm.js b/packages/kbn-es/src/utils/native_realm.js index 72bc471fd86f..247ddc461910 100644 --- a/packages/kbn-es/src/utils/native_realm.js +++ b/packages/kbn-es/src/utils/native_realm.js @@ -23,8 +23,16 @@ const chalk = require('chalk'); const { log: defaultLog } = require('./log'); exports.NativeRealm = class NativeRealm { - constructor(elasticPassword, port, log = defaultLog) { - this._client = new Client({ node: `http://elastic:${elasticPassword}@localhost:${port}` }); + constructor({ elasticPassword, port, log = defaultLog, ssl = false, caCert }) { + this._client = new Client({ + node: `${ssl ? 'https' : 'http'}://elastic:${elasticPassword}@localhost:${port}`, + ssl: ssl + ? { + ca: caCert, + rejectUnauthorized: true, + } + : undefined, + }); this._elasticPassword = elasticPassword; this._log = log; } diff --git a/packages/kbn-es/src/utils/native_realm.test.js b/packages/kbn-es/src/utils/native_realm.test.js index ae3c615f1ca2..99c7ed162301 100644 --- a/packages/kbn-es/src/utils/native_realm.test.js +++ b/packages/kbn-es/src/utils/native_realm.test.js @@ -40,7 +40,7 @@ const log = new ToolingLog(); let nativeRealm; beforeEach(() => { - nativeRealm = new NativeRealm('changeme', '9200', log); + nativeRealm = new NativeRealm({ elasticPassword: 'changeme', port: '9200', log }); }); afterAll(() => { diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json new file mode 100644 index 000000000000..6bb61453c99e --- /dev/null +++ b/packages/kbn-es/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/kbn-maki/index.js b/packages/kbn-maki/index.js deleted file mode 100644 index 8158f12794af..000000000000 --- a/packages/kbn-maki/index.js +++ /dev/null @@ -1,378 +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. - */ - -/* eslint-disable quotes */ - -// icons from maki version 6.1.0 -export const maki = { - svgArray: [ - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n \n \n \n", - "\n\n \n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n \n \n \n \n", - "\n\n \n \n \n \n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n" - ] -}; diff --git a/packages/kbn-maki/package.json b/packages/kbn-maki/package.json deleted file mode 100644 index 862e183800b3..000000000000 --- a/packages/kbn-maki/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@kbn/maki", - "version": "6.1.0", - "description": "browser friendly version of @mapbox/maki", - "license": "Apache-2.0", - "main": "index.js", - "devDependencies": {}, - "dependencies": {}, - "peerDependencies": {} -} diff --git a/packages/kbn-maki/readme.md b/packages/kbn-maki/readme.md deleted file mode 100644 index aad509938113..000000000000 --- a/packages/kbn-maki/readme.md +++ /dev/null @@ -1,6 +0,0 @@ -# @kbn/maki - -[@mapbox/maki](https://www.npmjs.com/package/@mapbox/maki) only works in node.js. -See https://github.com/mapbox/maki/issues/462 for details. - -@kbn/maki is a browser friendly version of @mapbox/maki diff --git a/packages/kbn-plugin-helpers/lib/utils.js b/packages/kbn-plugin-helpers/lib/utils.js index d9a5b9148208..6e3d8969802a 100644 --- a/packages/kbn-plugin-helpers/lib/utils.js +++ b/packages/kbn-plugin-helpers/lib/utils.js @@ -42,7 +42,7 @@ function resolveKibanaPath(path) { } function readFtrConfigFile(log, path, settingOverrides) { - return require(resolveKibanaPath('src/functional_test_runner')) // eslint-disable-line import/no-dynamic-require + return require('@kbn/test') // eslint-disable-line import/no-dynamic-require .readConfigFile(log, path, settingOverrides); } diff --git a/packages/kbn-test/src/es/es_test_cluster.js b/packages/kbn-test/src/es/es_test_cluster.js index be24c6dbf0f3..d1aa25591446 100644 --- a/packages/kbn-test/src/es/es_test_cluster.js +++ b/packages/kbn-test/src/es/es_test_cluster.js @@ -37,6 +37,8 @@ export function createEsTestCluster(options = {}) { basePath = resolve(KIBANA_ROOT, '.es'), esFrom = esTestConfig.getBuildFrom(), dataArchive, + esArgs, + ssl, } = options; const randomHash = Math.random() @@ -50,9 +52,10 @@ export function createEsTestCluster(options = {}) { password, license, basePath, + esArgs, }; - const cluster = new Cluster(log); + const cluster = new Cluster({ log, ssl }); return new (class EsTestCluster { getStartTimeout() { diff --git a/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js diff --git a/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/after_hook.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/after_hook.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/after_hook.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/after_hook.js diff --git a/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/before_hook.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/before_hook.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/before_hook.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/before_hook.js diff --git a/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/it.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/it.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/it.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/it.js diff --git a/src/functional_test_runner/__tests__/fixtures/simple_project/config.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/simple_project/config.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/simple_project/config.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/simple_project/config.js diff --git a/src/functional_test_runner/__tests__/fixtures/simple_project/tests.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/simple_project/tests.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/simple_project/tests.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/simple_project/tests.js diff --git a/src/functional_test_runner/__tests__/integration/basic.js b/packages/kbn-test/src/functional_test_runner/__tests__/integration/basic.js similarity index 99% rename from src/functional_test_runner/__tests__/integration/basic.js rename to packages/kbn-test/src/functional_test_runner/__tests__/integration/basic.js index 1c3858a8cfd6..711ce683ffdf 100644 --- a/src/functional_test_runner/__tests__/integration/basic.js +++ b/packages/kbn-test/src/functional_test_runner/__tests__/integration/basic.js @@ -25,7 +25,7 @@ import expect from '@kbn/expect'; const SCRIPT = resolve(__dirname, '../../../../scripts/functional_test_runner.js'); const BASIC_CONFIG = resolve(__dirname, '../fixtures/simple_project/config.js'); -describe('basic config file with a single app and test', function () { +describe('basic config file with a single app and test', function() { this.timeout(60 * 1000); it('runs and prints expected output', () => { diff --git a/src/functional_test_runner/__tests__/integration/failure_hooks.js b/packages/kbn-test/src/functional_test_runner/__tests__/integration/failure_hooks.js similarity index 87% rename from src/functional_test_runner/__tests__/integration/failure_hooks.js rename to packages/kbn-test/src/functional_test_runner/__tests__/integration/failure_hooks.js index 0c00e2771bb1..08e71e2e7324 100644 --- a/src/functional_test_runner/__tests__/integration/failure_hooks.js +++ b/packages/kbn-test/src/functional_test_runner/__tests__/integration/failure_hooks.js @@ -26,7 +26,7 @@ import expect from '@kbn/expect'; const SCRIPT = resolve(__dirname, '../../../../scripts/functional_test_runner.js'); const FAILURE_HOOKS_CONFIG = resolve(__dirname, '../fixtures/failure_hooks/config.js'); -describe('failure hooks', function () { +describe('failure hooks', function() { this.timeout(60 * 1000); it('runs and prints expected output', () => { @@ -37,8 +37,10 @@ describe('failure hooks', function () { flag: '$FAILING_BEFORE_HOOK$', assert(lines) { expect(lines.shift()).to.match(/info\s+testHookFailure\s+\$FAILING_BEFORE_ERROR\$/); - expect(lines.shift()).to.match(/info\s+testHookFailureAfterDelay\s+\$FAILING_BEFORE_ERROR\$/); - } + expect(lines.shift()).to.match( + /info\s+testHookFailureAfterDelay\s+\$FAILING_BEFORE_ERROR\$/ + ); + }, }, { flag: '$FAILING_TEST$', @@ -46,14 +48,16 @@ describe('failure hooks', function () { expect(lines.shift()).to.match(/global before each/); expect(lines.shift()).to.match(/info\s+testFailure\s+\$FAILING_TEST_ERROR\$/); expect(lines.shift()).to.match(/info\s+testFailureAfterDelay\s+\$FAILING_TEST_ERROR\$/); - } + }, }, { flag: '$FAILING_AFTER_HOOK$', assert(lines) { expect(lines.shift()).to.match(/info\s+testHookFailure\s+\$FAILING_AFTER_ERROR\$/); - expect(lines.shift()).to.match(/info\s+testHookFailureAfterDelay\s+\$FAILING_AFTER_ERROR\$/); - } + expect(lines.shift()).to.match( + /info\s+testHookFailureAfterDelay\s+\$FAILING_AFTER_ERROR\$/ + ); + }, }, ]; diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts new file mode 100644 index 000000000000..88b9f2008487 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { resolve } from 'path'; +import { run } from '../../../../src/dev/run'; +import { FunctionalTestRunner } from './functional_test_runner'; + +export function runFtrCli() { + run( + async ({ flags, log }) => { + const resolveConfigPath = (v: string) => resolve(process.cwd(), v); + const toArray = (v: string | string[]) => ([] as string[]).concat(v || []); + + const functionalTestRunner = new FunctionalTestRunner( + log, + resolveConfigPath(flags.config as string), + { + mochaOpts: { + bail: flags.bail, + grep: flags.grep || undefined, + invert: flags.invert, + }, + suiteTags: { + include: toArray(flags['include-tag'] as string | string[]), + exclude: toArray(flags['exclude-tag'] as string | string[]), + }, + updateBaselines: flags.updateBaselines, + excludeTestFiles: flags.exclude || undefined, + } + ); + + let teardownRun = false; + const teardown = async (err?: Error) => { + if (teardownRun) return; + + teardownRun = true; + if (err) { + log.indent(-log.indent()); + log.error(err); + process.exitCode = 1; + } + + try { + await functionalTestRunner.close(); + } finally { + process.exit(); + } + }; + + process.on('unhandledRejection', err => teardown(err)); + process.on('SIGTERM', () => teardown()); + process.on('SIGINT', () => teardown()); + + try { + if (flags['test-stats']) { + process.stderr.write( + JSON.stringify(await functionalTestRunner.getTestStats(), null, 2) + '\n' + ); + } else { + const failureCount = await functionalTestRunner.run(); + process.exitCode = failureCount ? 1 : 0; + } + } catch (err) { + await teardown(err); + } finally { + await teardown(); + } + }, + { + flags: { + string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag'], + boolean: ['bail', 'invert', 'test-stats', 'updateBaselines'], + default: { + config: 'test/functional/config.js', + debug: true, + }, + help: ` + --config=path path to a config file + --bail stop tests after the first failure + --grep pattern used to select which tests to run + --invert invert grep to exclude tests + --exclude=file path to a test file that should not be loaded + --include-tag=tag a tag to be included, pass multiple times for multiple tags + --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags + --test-stats print the number of tests (included and excluded) to STDERR + --updateBaselines replace baseline screenshots with whatever is generated from the test + `, + }, + } + ); +} diff --git a/src/functional_test_runner/fake_mocha_types.d.ts b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts similarity index 100% rename from src/functional_test_runner/fake_mocha_types.d.ts rename to packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts diff --git a/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts similarity index 100% rename from src/functional_test_runner/functional_test_runner.ts rename to packages/kbn-test/src/functional_test_runner/functional_test_runner.ts diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts new file mode 100644 index 000000000000..c783117a0ba4 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { FunctionalTestRunner } from './functional_test_runner'; +export { readConfigFile } from './lib'; +export { runFtrCli } from './cli'; diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.1.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.1.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.1.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.1.js diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.2.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.2.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.2.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.2.js diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.3.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.3.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.3.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.3.js diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.4.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.4.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.4.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.4.js diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.invalid.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.invalid.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.invalid.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.invalid.js diff --git a/src/functional_test_runner/lib/config/__tests__/read_config_file.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/read_config_file.js similarity index 92% rename from src/functional_test_runner/lib/config/__tests__/read_config_file.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/read_config_file.js index d4a08333e814..325e972a6cd9 100644 --- a/src/functional_test_runner/lib/config/__tests__/read_config_file.js +++ b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/read_config_file.js @@ -29,16 +29,14 @@ describe('readConfigFile()', () => { it('reads config from a file, returns an instance of Config class', async () => { const config = await readConfigFile(log, require.resolve('./fixtures/config.1')); expect(config).to.be.a(Config); - expect(config.get('testFiles')).to.eql([ - 'config.1' - ]); + expect(config.get('testFiles')).to.eql(['config.1']); }); it('merges setting overrides into log', async () => { const config = await readConfigFile(log, require.resolve('./fixtures/config.1'), { screenshots: { - directory: 'foo.bar' - } + directory: 'foo.bar', + }, }); expect(config.get('screenshots.directory')).to.be('foo.bar'); @@ -46,10 +44,7 @@ describe('readConfigFile()', () => { it('supports loading config files from within config files', async () => { const config = await readConfigFile(log, require.resolve('./fixtures/config.2')); - expect(config.get('testFiles')).to.eql([ - 'config.1', - 'config.2', - ]); + expect(config.get('testFiles')).to.eql(['config.1', 'config.2']); }); it('throws if settings are invalid', async () => { diff --git a/src/functional_test_runner/lib/config/config.ts b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts similarity index 100% rename from src/functional_test_runner/lib/config/config.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/config.ts diff --git a/src/functional_test_runner/lib/config/index.ts b/packages/kbn-test/src/functional_test_runner/lib/config/index.ts similarity index 100% rename from src/functional_test_runner/lib/config/index.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/index.ts diff --git a/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts similarity index 100% rename from src/functional_test_runner/lib/config/read_config_file.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts diff --git a/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts similarity index 99% rename from src/functional_test_runner/lib/config/schema.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 37bfbae2912f..4887ad2c6e1d 100644 --- a/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -178,6 +178,7 @@ export const schema = Joi.object() serverArgs: Joi.array(), serverEnvVars: Joi.object(), dataArchive: Joi.string(), + ssl: Joi.boolean().default(false), }) .default(), diff --git a/src/functional_test_runner/lib/config/transform_deprecations.ts b/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts similarity index 92% rename from src/functional_test_runner/lib/config/transform_deprecations.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts index 1b8538529b26..08dfc4a4f61f 100644 --- a/src/functional_test_runner/lib/config/transform_deprecations.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts @@ -18,7 +18,7 @@ */ // @ts-ignore -import { createTransform, Deprecations } from '../../../legacy/deprecation'; +import { createTransform, Deprecations } from '../../../../../../src/legacy/deprecation'; type DeprecationTransformer = ( settings: object, diff --git a/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts similarity index 100% rename from src/functional_test_runner/lib/index.ts rename to packages/kbn-test/src/functional_test_runner/lib/index.ts diff --git a/src/functional_test_runner/lib/lifecycle.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts similarity index 100% rename from src/functional_test_runner/lib/lifecycle.ts rename to packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts diff --git a/src/functional_test_runner/lib/load_tracer.ts b/packages/kbn-test/src/functional_test_runner/lib/load_tracer.ts similarity index 100% rename from src/functional_test_runner/lib/load_tracer.ts rename to packages/kbn-test/src/functional_test_runner/lib/load_tracer.ts diff --git a/src/functional_test_runner/lib/mocha/assignment_proxy.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/assignment_proxy.js similarity index 97% rename from src/functional_test_runner/lib/mocha/assignment_proxy.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/assignment_proxy.js index 70f8babfb29e..5c08d566d3d7 100644 --- a/src/functional_test_runner/lib/mocha/assignment_proxy.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/assignment_proxy.js @@ -31,7 +31,7 @@ export function createAssignmentProxy(object, interceptor) { get(target, property) { if (property === 'revertProxiedAssignments') { - return function () { + return function() { for (const [property, value] of originalValues) { object[property] = value; } @@ -39,6 +39,6 @@ export function createAssignmentProxy(object, interceptor) { } return Reflect.get(target, property); - } + }, }); } diff --git a/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js similarity index 91% rename from src/functional_test_runner/lib/mocha/decorate_mocha_ui.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js index a8272ae67717..e65eb2f27d06 100644 --- a/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js @@ -56,12 +56,12 @@ export function decorateMochaUi(lifecycle, context) { throw new Error(`Unexpected arguments to ${name}(${argumentsList.join(', ')})`); } - argumentsList[1] = function () { + argumentsList[1] = function() { before(async () => { await lifecycle.trigger('beforeTestSuite', this); }); - this.tags = (tags) => { + this.tags = tags => { this._tags = [].concat(this._tags || [], tags); }; @@ -77,7 +77,7 @@ export function decorateMochaUi(lifecycle, context) { }, after() { suiteLevel -= 1; - } + }, }); } @@ -91,9 +91,12 @@ export function decorateMochaUi(lifecycle, context) { * @return {Function} */ function wrapTestFunction(name, fn) { - return wrapNonSuiteFunction(name, wrapRunnableArgsWithErrorHandler(fn, async (err, test) => { - await lifecycle.trigger('testFailure', err, test); - })); + return wrapNonSuiteFunction( + name, + wrapRunnableArgsWithErrorHandler(fn, async (err, test) => { + await lifecycle.trigger('testFailure', err, test); + }) + ); } /** @@ -106,9 +109,12 @@ export function decorateMochaUi(lifecycle, context) { * @return {Function} */ function wrapTestHookFunction(name, fn) { - return wrapNonSuiteFunction(name, wrapRunnableArgsWithErrorHandler(fn, async (err, test) => { - await lifecycle.trigger('testHookFailure', err, test); - })); + return wrapNonSuiteFunction( + name, + wrapRunnableArgsWithErrorHandler(fn, async (err, test) => { + await lifecycle.trigger('testHookFailure', err, test); + }) + ); } /** @@ -127,7 +133,7 @@ export function decorateMochaUi(lifecycle, context) { All ${name}() calls in test files must be within a describe() call. `); } - } + }, }); } diff --git a/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js similarity index 92% rename from src/functional_test_runner/lib/mocha/filter_suites_by_tags.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js index 597be9bab32a..302d43fac3e6 100644 --- a/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js @@ -30,18 +30,15 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { mocha.excludedTests = []; // collect all the tests from some suite, including it's children - const collectTests = (suite) => - suite.suites.reduce( - (acc, s) => acc.concat(collectTests(s)), - suite.tests - ); + const collectTests = suite => + suite.suites.reduce((acc, s) => acc.concat(collectTests(s)), suite.tests); // if include tags were provided, filter the tree once to // only include branches that are included at some point if (include.length) { log.info('Only running suites (and their sub-suites) if they include the tag(s):', include); - const isIncluded = suite => !suite._tags ? false : suite._tags.some(t => include.includes(t)); + const isIncluded = suite => (!suite._tags ? false : suite._tags.some(t => include.includes(t))); const isChildIncluded = suite => suite.suites.some(s => isIncluded(s) || isChildIncluded(s)); (function recurse(parentSuite) { @@ -55,7 +52,6 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { continue; } - // this suite has an included child but is not included // itself, so strip out its tests and recurse to filter // out child suites which are not included @@ -69,10 +65,9 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { mocha.excludedTests = mocha.excludedTests.concat(collectTests(child)); } } - }(mocha.suite)); + })(mocha.suite); } - // if exclude tags were provided, filter the possibly already // filtered tree to remove branches that are excluded if (exclude.length) { @@ -94,6 +89,6 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { mocha.excludedTests = mocha.excludedTests.concat(collectTests(child)); } } - }(mocha.suite)); + })(mocha.suite); } } diff --git a/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js similarity index 100% rename from src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js diff --git a/src/functional_test_runner/lib/mocha/index.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/index.ts similarity index 100% rename from src/functional_test_runner/lib/mocha/index.ts rename to packages/kbn-test/src/functional_test_runner/lib/mocha/index.ts diff --git a/src/functional_test_runner/lib/mocha/load_test_files.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js similarity index 87% rename from src/functional_test_runner/lib/mocha/load_test_files.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js index c876c39b9c64..70b0c0874e5e 100644 --- a/src/functional_test_runner/lib/mocha/load_test_files.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js @@ -31,10 +31,18 @@ import { decorateMochaUi } from './decorate_mocha_ui'; * @param {String} path * @return {undefined} - mutates mocha, no return value */ -export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, excludePaths, updateBaselines }) => { +export const loadTestFiles = ({ + mocha, + log, + lifecycle, + providers, + paths, + excludePaths, + updateBaselines, +}) => { const pendingExcludes = new Set(excludePaths.slice(0)); - const innerLoadTestFile = (path) => { + const innerLoadTestFile = path => { if (typeof path !== 'string' || !isAbsolute(path)) { throw new TypeError('loadTestFile() only accepts absolute paths'); } @@ -49,9 +57,7 @@ export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, exclude log.verbose('Loading test file %s', path); const testModule = require(path); // eslint-disable-line import/no-dynamic-require - const testProvider = testModule.__esModule - ? testModule.default - : testModule; + const testProvider = testModule.__esModule ? testModule.default : testModule; runTestProvider(testProvider, path); // eslint-disable-line }); @@ -90,6 +96,11 @@ export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, exclude paths.forEach(innerLoadTestFile); if (pendingExcludes.size) { - throw new Error(`After loading all test files some exclude paths were not consumed:${['', ...pendingExcludes].join('\n -')}`); + throw new Error( + `After loading all test files some exclude paths were not consumed:${[ + '', + ...pendingExcludes, + ].join('\n -')}` + ); } }; diff --git a/src/functional_test_runner/lib/mocha/reporter/colors.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/colors.js similarity index 100% rename from src/functional_test_runner/lib/mocha/reporter/colors.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/colors.js diff --git a/src/functional_test_runner/lib/mocha/reporter/index.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/index.js similarity index 100% rename from src/functional_test_runner/lib/mocha/reporter/index.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/index.js diff --git a/src/functional_test_runner/lib/mocha/reporter/ms.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ms.js similarity index 100% rename from src/functional_test_runner/lib/mocha/reporter/ms.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ms.js diff --git a/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js similarity index 83% rename from src/functional_test_runner/lib/mocha/reporter/reporter.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js index 386411631c15..969ccff93f3b 100644 --- a/src/functional_test_runner/lib/mocha/reporter/reporter.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js @@ -23,12 +23,12 @@ import Mocha from 'mocha'; import { ToolingLogTextWriter } from '@kbn/dev-utils'; import moment from 'moment'; -import { setupJUnitReportGeneration } from '../../../../dev'; +import { setupJUnitReportGeneration } from '../../../../../../../src/dev'; import * as colors from './colors'; import * as symbols from './symbols'; import { ms } from './ms'; import { writeEpilogue } from './write_epilogue'; -import { recordLog, snapshotLogsForRunnable } from '../../../../dev/mocha/log_cache'; +import { recordLog, snapshotLogsForRunnable } from '../../../../../../../src/dev/mocha/log_cache'; export function MochaReporterProvider({ getService }) { const log = getService('log'); @@ -60,7 +60,9 @@ export function MochaReporterProvider({ getService }) { onStart = () => { if (config.get('mochaReporter.captureLogOutput')) { - log.warning('debug logs are being captured, only error logs will be written to the console'); + log.warning( + 'debug logs are being captured, only error logs will be written to the console' + ); reporterCaptureStartTime = moment(); originalLogWriters = log.getWriters(); @@ -68,47 +70,47 @@ export function MochaReporterProvider({ getService }) { log.setWriters([ new ToolingLogTextWriter({ level: 'error', - writeTo: process.stdout + writeTo: process.stdout, }), new ToolingLogTextWriter({ level: 'debug', writeTo: { - write: (line) => { + write: line => { // if the current runnable is a beforeEach hook then // `runner.suite` is set to the suite that defined the // hook, rather than the suite executing, so instead we // grab the suite from the test, but that's only available // when we are doing something test specific, so for global // hooks we fallback to `runner.suite` - const currentSuite = this.runner.test - ? this.runner.test.parent - : this.runner.suite; + const currentSuite = this.runner.test ? this.runner.test.parent : this.runner.suite; // We are computing the difference between the time when this // reporter has started and the time when each line are being // logged in order to be able to label the test results log lines // with this relative time information const diffTimeSinceStart = moment().diff(reporterCaptureStartTime); - const readableDiffTimeSinceStart = `[${moment(diffTimeSinceStart).format('HH:mm:ss')}] `; + const readableDiffTimeSinceStart = `[${moment(diffTimeSinceStart).format( + 'HH:mm:ss' + )}] `; recordLog(currentSuite, `${readableDiffTimeSinceStart} ${line}`); - } - } - }) + }, + }, + }), ]); } log.write(''); - } + }; onHookStart = hook => { log.write(`-> ${colors.suite(hook.title)}`); log.indent(2); - } + }; onHookEnd = () => { log.indent(-2); - } + }; onSuiteStart = suite => { if (!suite.root) { @@ -116,34 +118,34 @@ export function MochaReporterProvider({ getService }) { } log.indent(2); - } + }; onSuiteEnd = () => { if (log.indent(-2) === 0) { log.write(); } - } + }; onTestStart = test => { log.write(`-> ${test.title}`); log.indent(2); - } + }; - onTestEnd = (test) => { + onTestEnd = test => { snapshotLogsForRunnable(test); log.indent(-2); - } + }; onPending = test => { log.write('-> ' + colors.pending(test.title)); log.indent(2); - } + }; onPass = test => { const time = colors.speed(test.speed, ` (${ms(test.duration)})`); const pass = colors.pass(`${symbols.ok} pass`); log.write(`- ${pass} ${time} "${test.fullTitle()}"`); - } + }; onFail = runnable => { // NOTE: this is super gross @@ -155,7 +157,7 @@ export function MochaReporterProvider({ getService }) { // let output = ''; const realLog = console.log; - console.log = (...args) => output += `${format(...args)}\n`; + console.log = (...args) => (output += `${format(...args)}\n`); try { Mocha.reporters.Base.list([runnable]); } finally { @@ -164,21 +166,21 @@ export function MochaReporterProvider({ getService }) { log.write( `- ${colors.fail(`${symbols.err} fail: "${runnable.fullTitle()}"`)}` + - '\n' + - output - .split('\n') - // drop the first two lines, (empty + test title) - .slice(2) - // move leading colors behind leading spaces - .map(line => line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1')) - .map(line => ` ${line}`) - .join('\n') + '\n' + + output + .split('\n') + // drop the first two lines, (empty + test title) + .slice(2) + // move leading colors behind leading spaces + .map(line => line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1')) + .map(line => ` ${line}`) + .join('\n') ); // failed hooks trigger the `onFail(runnable)` callback, so we snapshot the logs for // them here. Tests will re-capture the snapshot in `onTestEnd()` snapshotLogsForRunnable(runnable); - } + }; onEnd = () => { if (originalLogWriters) { @@ -186,6 +188,6 @@ export function MochaReporterProvider({ getService }) { } writeEpilogue(log, this.stats); - } + }; }; } diff --git a/src/functional_test_runner/lib/mocha/reporter/symbols.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/symbols.js similarity index 85% rename from src/functional_test_runner/lib/mocha/reporter/symbols.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/symbols.js index 4f69229f0f1a..9819343ff953 100644 --- a/src/functional_test_runner/lib/mocha/reporter/symbols.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/symbols.js @@ -19,10 +19,6 @@ // originally extracted from mocha https://git.io/v1PGh -export const ok = process.platform === 'win32' - ? '\u221A' - : '✓'; +export const ok = process.platform === 'win32' ? '\u221A' : '✓'; -export const err = process.platform === 'win32' - ? '\u00D7' - : '✖'; +export const err = process.platform === 'win32' ? '\u00D7' : '✖'; diff --git a/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js similarity index 81% rename from src/functional_test_runner/lib/mocha/reporter/write_epilogue.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js index 09964de3e8a3..003827120e93 100644 --- a/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js @@ -25,26 +25,16 @@ export function writeEpilogue(log, stats) { log.write(); // passes - log.write( - `${colors.pass('%d passing')} (%s)`, - stats.passes || 0, - ms(stats.duration) - ); + log.write(`${colors.pass('%d passing')} (%s)`, stats.passes || 0, ms(stats.duration)); // pending if (stats.pending) { - log.write( - colors.pending('%d pending'), - stats.pending - ); + log.write(colors.pending('%d pending'), stats.pending); } // failures if (stats.failures) { - log.write( - '%d failing', - stats.failures - ); + log.write('%d failing', stats.failures); } // footer diff --git a/src/functional_test_runner/lib/mocha/run_tests.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/run_tests.ts similarity index 100% rename from src/functional_test_runner/lib/mocha/run_tests.ts rename to packages/kbn-test/src/functional_test_runner/lib/mocha/run_tests.ts diff --git a/src/functional_test_runner/lib/mocha/setup_mocha.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js similarity index 91% rename from src/functional_test_runner/lib/mocha/setup_mocha.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js index c39db58cf9a5..6746914fe031 100644 --- a/src/functional_test_runner/lib/mocha/setup_mocha.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js @@ -36,14 +36,11 @@ export async function setupMocha(lifecycle, log, config, providers) { // configure mocha const mocha = new Mocha({ ...config.get('mochaOpts'), - reporter: await providers.loadExternalService( - 'mocha reporter', - MochaReporterProvider - ) + reporter: await providers.loadExternalService('mocha reporter', MochaReporterProvider), }); // global beforeEach hook in root suite triggers before all others - mocha.suite.beforeEach('global before each', async function () { + mocha.suite.beforeEach('global before each', async function() { await lifecycle.trigger('beforeEachTest', this.currentTest); }); diff --git a/src/functional_test_runner/lib/mocha/wrap_function.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_function.js similarity index 99% rename from src/functional_test_runner/lib/mocha/wrap_function.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_function.js index b9fe0be4c1af..eac5200a4174 100644 --- a/src/functional_test_runner/lib/mocha/wrap_function.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_function.js @@ -17,7 +17,6 @@ * under the License. */ - /** * Get handler that will intercept calls to `toString` * on the function, since Function.prototype.toString() @@ -59,7 +58,7 @@ export function wrapFunction(fn, hooks = {}) { hooks.after(target, thisArg, argumentsList); } } - } + }, }); } @@ -94,6 +93,6 @@ export function wrapAsyncFunction(fn, hooks = {}) { await hooks.after(target, thisArg, argumentsList); } } - } + }, }); } diff --git a/src/functional_test_runner/lib/mocha/wrap_runnable_args.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_runnable_args.js similarity index 99% rename from src/functional_test_runner/lib/mocha/wrap_runnable_args.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_runnable_args.js index 39c956ea22aa..5ee21e81e83c 100644 --- a/src/functional_test_runner/lib/mocha/wrap_runnable_args.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_runnable_args.js @@ -36,7 +36,7 @@ export function wrapRunnableArgsWithErrorHandler(fn, handler) { argumentsList[i] = wrapRunnableError(argumentsList[i], handler); } } - } + }, }); } @@ -45,6 +45,6 @@ function wrapRunnableError(runnable, handler) { async handleError(target, thisArg, argumentsList, err) { await handler(err, thisArg.test); throw err; - } + }, }); } diff --git a/src/functional_test_runner/lib/providers/async_instance.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/async_instance.ts similarity index 100% rename from src/functional_test_runner/lib/providers/async_instance.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/async_instance.ts diff --git a/src/functional_test_runner/lib/providers/index.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts similarity index 100% rename from src/functional_test_runner/lib/providers/index.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/index.ts diff --git a/src/functional_test_runner/lib/providers/provider_collection.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts similarity index 100% rename from src/functional_test_runner/lib/providers/provider_collection.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts diff --git a/src/functional_test_runner/lib/providers/read_provider_spec.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/read_provider_spec.ts similarity index 100% rename from src/functional_test_runner/lib/providers/read_provider_spec.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/read_provider_spec.ts diff --git a/src/functional_test_runner/lib/providers/verbose_instance.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/verbose_instance.ts similarity index 100% rename from src/functional_test_runner/lib/providers/verbose_instance.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/verbose_instance.ts diff --git a/packages/kbn-test/src/functional_tests/lib/auth.js b/packages/kbn-test/src/functional_tests/lib/auth.js index b696ab8f4c12..358b16c562b1 100644 --- a/packages/kbn-test/src/functional_tests/lib/auth.js +++ b/packages/kbn-test/src/functional_tests/lib/auth.js @@ -17,6 +17,8 @@ * under the License. */ +import fs from 'fs'; +import util from 'util'; import { format as formatUrl } from 'url'; import request from 'request'; @@ -24,13 +26,23 @@ import { delay } from 'bluebird'; export const DEFAULT_SUPERUSER_PASS = 'changeme'; -async function updateCredentials(port, auth, username, password, retries = 10) { +const readFile = util.promisify(fs.readFile); + +async function updateCredentials({ + port, + auth, + username, + password, + retries = 10, + protocol, + caCert, +}) { const result = await new Promise((resolve, reject) => request( { method: 'PUT', uri: formatUrl({ - protocol: 'http:', + protocol: `${protocol}:`, auth, hostname: 'localhost', port, @@ -38,6 +50,7 @@ async function updateCredentials(port, auth, username, password, retries = 10) { }), json: true, body: { password }, + ca: caCert, }, (err, httpResponse, body) => { if (err) return reject(err); @@ -55,26 +68,35 @@ async function updateCredentials(port, auth, username, password, retries = 10) { if (retries > 0) { await delay(2500); - return await updateCredentials(port, auth, username, password, retries - 1); + return await updateCredentials({ + port, + auth, + username, + password, + retries: retries - 1, + protocol, + caCert, + }); } throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`); } -export async function setupUsers(log, esPort, updates) { +export async function setupUsers({ log, esPort, updates, protocol = 'http', caPath }) { // track the current credentials for the `elastic` user as // they will likely change as we apply updates let auth = `elastic:${DEFAULT_SUPERUSER_PASS}`; + const caCert = caPath && (await readFile(caPath)); for (const { username, password, roles } of updates) { // If working with a built-in user, just change the password if (['logstash_system', 'elastic', 'kibana'].includes(username)) { - await updateCredentials(esPort, auth, username, password); + await updateCredentials({ port: esPort, auth, username, password, protocol, caCert }); log.info('setting %j user password to %j', username, password); // If not a builtin user, add them } else { - await insertUser(esPort, auth, username, password, roles); + await insertUser({ port: esPort, auth, username, password, roles, protocol, caCert }); log.info('Added %j user with password to %j', username, password); } @@ -84,13 +106,22 @@ export async function setupUsers(log, esPort, updates) { } } -async function insertUser(port, auth, username, password, roles = [], retries = 10) { +async function insertUser({ + port, + auth, + username, + password, + roles = [], + retries = 10, + protocol, + caCert, +}) { const result = await new Promise((resolve, reject) => request( { method: 'POST', uri: formatUrl({ - protocol: 'http:', + protocol: `${protocol}:`, auth, hostname: 'localhost', port, @@ -98,6 +129,7 @@ async function insertUser(port, auth, username, password, roles = [], retries = }), json: true, body: { password, roles }, + ca: caCert, }, (err, httpResponse, body) => { if (err) return reject(err); @@ -114,7 +146,16 @@ async function insertUser(port, auth, username, password, roles = [], retries = if (retries > 0) { await delay(2500); - return await insertUser(port, auth, username, password, retries - 1); + return await insertUser({ + port, + auth, + username, + password, + roles, + retries: retries - 1, + protocol, + caCert, + }); } throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`); diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js index 2e290222b1a9..2423665bacb8 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.js @@ -25,6 +25,7 @@ import { setupUsers, DEFAULT_SUPERUSER_PASS } from './auth'; export async function runElasticsearch({ config, options }) { const { log, esFrom } = options; + const ssl = config.get('esTestCluster.ssl'); const license = config.get('esTestCluster.license'); const esArgs = config.get('esTestCluster.serverArgs'); const esEnvVars = config.get('esTestCluster.serverEnvVars'); @@ -40,16 +41,28 @@ export async function runElasticsearch({ config, options }) { basePath: resolve(KIBANA_ROOT, '.es'), esFrom: esFrom || config.get('esTestCluster.from'), dataArchive: config.get('esTestCluster.dataArchive'), + esArgs, + ssl, }); await cluster.start(esArgs, esEnvVars); if (isSecurityEnabled) { - await setupUsers(log, config.get('servers.elasticsearch.port'), [ - config.get('servers.elasticsearch'), - config.get('servers.kibana'), - ]); + await setupUsers({ + log, + esPort: config.get('servers.elasticsearch.port'), + updates: [config.get('servers.elasticsearch'), config.get('servers.kibana')], + protocol: config.get('servers.elasticsearch').protocol, + caPath: getRelativeCertificateAuthorityPath(config.get('kbnTestServer.serverArgs')), + }); } return cluster; } + +function getRelativeCertificateAuthorityPath(esConfig = []) { + const caConfig = esConfig.find( + config => config.indexOf('--elasticsearch.ssl.certificateAuthorities') === 0 + ); + return caConfig ? caConfig.split('=')[1] : undefined; +} diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index 8b347d9dc595..a0edfcdb8c7b 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -17,7 +17,7 @@ * under the License. */ -import { FunctionalTestRunner, readConfigFile } from '../../../../../src/functional_test_runner'; +import { FunctionalTestRunner, readConfigFile } from '../../functional_test_runner'; import { CliError } from './run_cli'; async function createFtr({ configPath, options: { log, bail, grep, updateBaselines, suiteTags } }) { diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 76afe15e9c0a..966b148f0ce6 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -31,7 +31,7 @@ import { KIBANA_FTR_SCRIPT, } from './lib'; -import { readConfigFile } from '../../../../src/functional_test_runner/lib'; +import { readConfigFile } from '../functional_test_runner/lib'; const SUCCESS_MESSAGE = ` diff --git a/packages/kbn-test/src/index.js b/packages/kbn-test/src/index.js index add8b470b1c8..e8cc694f5252 100644 --- a/packages/kbn-test/src/index.js +++ b/packages/kbn-test/src/index.js @@ -28,3 +28,7 @@ export { esTestConfig, createEsTestCluster } from './es'; export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn'; export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth'; + +export { readConfigFile } from './functional_test_runner/lib/config/read_config_file'; + +export { runFtrCli } from './functional_test_runner/cli'; diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 8d4281850228..83a0fe04a4b5 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "include": [ - "types/**/*" + "types/**/*", + "src/functional_test_runner/**/*" ] } diff --git a/packages/kbn-test/types/ftr.d.ts b/packages/kbn-test/types/ftr.d.ts index d85363892c99..8289877ed1b9 100644 --- a/packages/kbn-test/types/ftr.d.ts +++ b/packages/kbn-test/types/ftr.d.ts @@ -18,7 +18,7 @@ */ import { ToolingLog } from '@kbn/dev-utils'; -import { Config, Lifecycle } from '../../../src/functional_test_runner/lib'; +import { Config, Lifecycle } from '../src/functional_test_runner/lib'; interface AsyncInstance { /** diff --git a/packages/kbn-ui-framework/dist/kui_dark.css b/packages/kbn-ui-framework/dist/kui_dark.css index 7a962b510fc0..dcbd65fbca52 100644 --- a/packages/kbn-ui-framework/dist/kui_dark.css +++ b/packages/kbn-ui-framework/dist/kui_dark.css @@ -492,29 +492,6 @@ main { .kuiCollapseButton:hover { opacity: 1; } -.kuiExpression { - padding: 20px; - white-space: nowrap; } - -.kuiExpressionButton { - background-color: transparent; - padding: 5px 0px; - border: none; - border-bottom: dotted 2px #343741; - font-size: 16px; - cursor: pointer; } - -.kuiExpressionButton__description { - color: #7DE2D1; - text-transform: uppercase; } - -.kuiExpressionButton__value { - color: #DFE5EF; - text-transform: lowercase; } - -.kuiExpressionButton-isActive { - border-bottom: solid 2px #7DE2D1; } - /** * 1. Set inline-block so this wrapper shrinks to fit the input. */ @@ -1635,262 +1612,6 @@ main { padding: 0; display: none; } -/** - * 1. Allow class to be applied to `ul` and `ol` elements - */ -.kuiMenu { - padding-left: 0; - /* 1 */ } - -.kuiMenu--contained { - border: 1px solid #343741; } - .kuiMenu--contained .kuiMenuItem { - padding: 6px 10px; } - -/** - * 1. Allow class to be applied to `li` elements - */ -.kuiMenuItem { - list-style: none; - /* 1 */ - padding: 6px 0; } - .kuiMenuItem + .kuiMenuItem { - border-top: 1px solid #343741; } - -/** - * 1. Setting to inline-block guarantees the same height when applied to both - * button elements and anchor tags. - * 2. Disable for Angular. - * 3. Make the button just tall enough to fit inside an Option Layout. - */ -.kuiMenuButton { - display: inline-block; - /* 1 */ - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - padding: 2px 10px; - /* 3 */ - font-size: 12px; - font-weight: 400; - line-height: 1.5; - text-decoration: none; - border: none; - border-radius: 4px; } - .kuiMenuButton:disabled { - cursor: default; - pointer-events: none; - /* 2 */ } - .kuiMenuButton:active:enabled { - -webkit-transform: translateY(1px); - transform: translateY(1px); } - .kuiMenuButton:focus { - z-index: 1; - /* 1 */ - outline: none !important; - /* 2 */ - -webkit-box-shadow: 0 0 0 1px #1D1E24, 0 0 0 2px #1BA9F5; - box-shadow: 0 0 0 1px #1D1E24, 0 0 0 2px #1BA9F5; - /* 3 */ } - -.kuiMenuButton--iconText .kuiMenuButton__icon:first-child { - margin-right: 4px; } - -.kuiMenuButton--iconText .kuiMenuButton__icon:last-child { - margin-left: 4px; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--basic { - color: #DFE5EF; - background-color: #1D1E24; } - .kuiMenuButton--basic:focus { - color: #DFE5EF !important; - /* 1 */ } - .kuiMenuButton--basic:hover, .kuiMenuButton--basic:active { - /* 2 */ - color: #DFE5EF !important; - /* 1 */ - background-color: #25262E; } - .kuiMenuButton--basic:disabled { - color: #535966; - cursor: not-allowed; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--primary { - color: #FFF; - background-color: #1BA9F5; } - .kuiMenuButton--primary:focus { - color: #FFF !important; - /* 1 */ } - .kuiMenuButton--primary:hover, .kuiMenuButton--primary:active { - /* 2 */ - color: #FFF !important; - /* 1 */ - background-color: #098dd4; } - .kuiMenuButton--primary:disabled { - background-color: #535966; - cursor: not-allowed; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--danger { - color: #FFF; - background-color: #F66; } - .kuiMenuButton--danger:hover, .kuiMenuButton--danger:active { - /* 2 */ - color: #FFF !important; - /* 1 */ - background-color: #ff3333; } - .kuiMenuButton--danger:disabled { - color: #FFF; - background-color: #535966; - cursor: not-allowed; } - .kuiMenuButton--danger:focus { - z-index: 1; - /* 1 */ - outline: none !important; - /* 2 */ - -webkit-box-shadow: 0 0 0 1px #1D1E24, 0 0 0 2px #F66; - box-shadow: 0 0 0 1px #1D1E24, 0 0 0 2px #F66; - /* 3 */ } - -.kuiMenuButtonGroup { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; } - .kuiMenuButtonGroup .kuiMenuButton + .kuiMenuButton { - margin-left: 4px; } - -.kuiMenuButtonGroup--alignRight { - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; } - -.kuiModalOverlay { - position: fixed; - z-index: 1000; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - padding-bottom: 10vh; - background: rgba(52, 55, 65, 0.8); } - -.kuiModal { - -webkit-box-shadow: 0 12px 24px 0 rgba(0, 0, 0, 0.1), 0 6px 12px 0 rgba(0, 0, 0, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.1), 0 2px 2px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 12px 24px 0 rgba(0, 0, 0, 0.1), 0 6px 12px 0 rgba(0, 0, 0, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.1), 0 2px 2px 0 rgba(0, 0, 0, 0.1); - line-height: 1.5; - background-color: #1D1E24; - border: 1px solid #343741; - border-radius: 4px; - z-index: 1001; - -webkit-animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); - animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); } - -.kuiModal--confirmation { - width: 450px; - min-width: auto; } - -.kuiModalHeader { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; - padding: 10px; - padding-left: 20px; - border-bottom: 1px solid #343741; } - -.kuiModalHeader__title { - font-size: 20px; } - -.kuiModalHeaderCloseButton { - display: inline-block; - /* 1 */ - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - padding: 2px 5px; - border: 1px solid transparent; - color: #98A2B3; - background-color: transparent; - line-height: 1; - /* 2 */ - font-size: 20px; } - .kuiModalHeaderCloseButton:hover { - color: #DFE5EF; } - -.kuiModalBody { - padding: 20px; } - -.kuiModalBodyText { - font-size: 14px; } - -.kuiModalFooter { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; - padding: 20px; - padding-top: 10px; } - .kuiModalFooter > * + * { - margin-left: 5px; } - -@-webkit-keyframes kuiModal { - 0% { - opacity: 0; - -webkit-transform: translateY(32px); - transform: translateY(32px); } - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); } } - -@keyframes kuiModal { - 0% { - opacity: 0; - -webkit-transform: translateY(32px); - transform: translateY(32px); } - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); } } - /** * 1. Put 10px of space between each child. */ @@ -2090,107 +1811,6 @@ main { .kuiPanelBody { padding: 10px; } -.kuiPanelSimple { - -webkit-box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3), 0 1px 5px -2px rgba(0, 0, 0, 0.3); - box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3), 0 1px 5px -2px rgba(0, 0, 0, 0.3); - background-color: #1D1E24; - border: 1px solid #343741; - border-radius: 4px; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; } - .kuiPanelSimple.kuiPanelSimple--paddingSmall { - padding: 8px; } - .kuiPanelSimple.kuiPanelSimple--paddingMedium { - padding: 16px; } - .kuiPanelSimple.kuiPanelSimple--paddingLarge { - padding: 24px; } - .kuiPanelSimple.kuiPanelSimple--shadow { - -webkit-box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); - box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); } - .kuiPanelSimple.kuiPanelSimple--flexGrowZero { - -webkit-box-flex: 0; - -webkit-flex-grow: 0; - -ms-flex-positive: 0; - flex-grow: 0; } - -.kuiPopover { - display: inline-block; - position: relative; } - .kuiPopover.kuiPopover-isOpen .kuiPopover__panel { - opacity: 1; - visibility: visible; - margin-top: 8px; - pointer-events: auto; } - -.kuiPopover__panel { - position: absolute; - z-index: 2000; - top: 100%; - left: 50%; - -webkit-transform: translateX(-50%) translateY(8px) translateZ(0); - transform: translateX(-50%) translateY(8px) translateZ(0); - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; - transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; - -webkit-transform-origin: center top; - transform-origin: center top; - opacity: 0; - visibility: hidden; - pointer-events: none; - margin-top: 24px; } - .kuiPopover__panel:before { - position: absolute; - content: ""; - top: -16px; - height: 0; - width: 0; - left: 50%; - margin-left: -16px; - border-left: 16px solid transparent; - border-right: 16px solid transparent; - border-bottom: 16px solid #343741; } - .kuiPopover__panel:after { - position: absolute; - content: ""; - top: -15px; - right: 0; - height: 0; - left: 50%; - margin-left: -16px; - width: 0; - border-left: 16px solid transparent; - border-right: 16px solid transparent; - border-bottom: 16px solid #1D1E24; } - -.kuiPopover--withTitle .kuiPopover__panel:after { - border-bottom-color: #25262E; } - -.kuiPopover--anchorLeft .kuiPopover__panel { - left: 0; - -webkit-transform: translateX(0%) translateY(8px) translateZ(0); - transform: translateX(0%) translateY(8px) translateZ(0); } - .kuiPopover--anchorLeft .kuiPopover__panel:before, .kuiPopover--anchorLeft .kuiPopover__panel:after { - right: auto; - left: 16px; - margin: 0; } - -.kuiPopover--anchorRight .kuiPopover__panel { - left: 100%; - -webkit-transform: translateX(-100%) translateY(8px) translateZ(0); - transform: translateX(-100%) translateY(8px) translateZ(0); } - .kuiPopover--anchorRight .kuiPopover__panel:before, .kuiPopover--anchorRight .kuiPopover__panel:after { - right: 16px; - left: auto; } - -.kuiPopoverTitle { - background-color: #25262E; - border-bottom: 1px solid #343741; - padding: 12px; - font-size: 16px; } - .kuiEmptyTablePrompt { display: -webkit-box; display: -webkit-flex; @@ -2497,57 +2117,6 @@ main { background-color: transparent; border-bottom-color: transparent; } -/** - * 1. Allow container to determine font-size and line-height. - * 2. Override inherited Bootstrap styles. - */ -.kuiToggleButton { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - background-color: transparent; - border: none; - padding: 0; - font-size: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - color: #DFE5EF; } - .kuiToggleButton:focus { - color: #DFE5EF; } - .kuiToggleButton:active { - color: #1BA9F5 !important; - /* 2 */ } - .kuiToggleButton:hover:not(:disabled) { - color: #098dd4 !important; - /* 2 */ - text-decoration: underline; } - .kuiToggleButton:disabled { - cursor: not-allowed; - opacity: .5; } - -/** - * 1. Make icon a consistent width so the text doesn't get pushed around as the icon changes - * between "expand" and "collapse". Use ems to be relative to inherited font-size. - */ -.kuiToggleButton__icon { - width: 0.8em; - /* 1 */ } - -.kuiTogglePanelHeader { - padding-bottom: 4px; - margin-bottom: 15px; - border-bottom: 1px solid #343741; - /** - * 1. Allow the user to click anywhere on the header, not just on the button text. - */ } - .kuiTogglePanelHeader .kuiToggleButton { - width: 100%; - /* 1 */ - text-align: left; - /* 1 */ } - .kuiToolBar { display: -webkit-box; display: -webkit-flex; diff --git a/packages/kbn-ui-framework/dist/kui_light.css b/packages/kbn-ui-framework/dist/kui_light.css index 8668bac99b64..3e82873d53aa 100644 --- a/packages/kbn-ui-framework/dist/kui_light.css +++ b/packages/kbn-ui-framework/dist/kui_light.css @@ -492,29 +492,6 @@ main { .kuiCollapseButton:hover { opacity: 1; } -.kuiExpression { - padding: 20px; - white-space: nowrap; } - -.kuiExpressionButton { - background-color: transparent; - padding: 5px 0px; - border: none; - border-bottom: dotted 2px #D3DAE6; - font-size: 16px; - cursor: pointer; } - -.kuiExpressionButton__description { - color: #017D73; - text-transform: uppercase; } - -.kuiExpressionButton__value { - color: #343741; - text-transform: lowercase; } - -.kuiExpressionButton-isActive { - border-bottom: solid 2px #017D73; } - /** * 1. Set inline-block so this wrapper shrinks to fit the input. */ @@ -1635,262 +1612,6 @@ main { padding: 0; display: none; } -/** - * 1. Allow class to be applied to `ul` and `ol` elements - */ -.kuiMenu { - padding-left: 0; - /* 1 */ } - -.kuiMenu--contained { - border: 1px solid #D3DAE6; } - .kuiMenu--contained .kuiMenuItem { - padding: 6px 10px; } - -/** - * 1. Allow class to be applied to `li` elements - */ -.kuiMenuItem { - list-style: none; - /* 1 */ - padding: 6px 0; } - .kuiMenuItem + .kuiMenuItem { - border-top: 1px solid #D3DAE6; } - -/** - * 1. Setting to inline-block guarantees the same height when applied to both - * button elements and anchor tags. - * 2. Disable for Angular. - * 3. Make the button just tall enough to fit inside an Option Layout. - */ -.kuiMenuButton { - display: inline-block; - /* 1 */ - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - padding: 2px 10px; - /* 3 */ - font-size: 12px; - font-weight: 400; - line-height: 1.5; - text-decoration: none; - border: none; - border-radius: 4px; } - .kuiMenuButton:disabled { - cursor: default; - pointer-events: none; - /* 2 */ } - .kuiMenuButton:active:enabled { - -webkit-transform: translateY(1px); - transform: translateY(1px); } - .kuiMenuButton:focus { - z-index: 1; - /* 1 */ - outline: none !important; - /* 2 */ - -webkit-box-shadow: 0 0 0 1px #FFF, 0 0 0 2px #006BB4; - box-shadow: 0 0 0 1px #FFF, 0 0 0 2px #006BB4; - /* 3 */ } - -.kuiMenuButton--iconText .kuiMenuButton__icon:first-child { - margin-right: 4px; } - -.kuiMenuButton--iconText .kuiMenuButton__icon:last-child { - margin-left: 4px; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--basic { - color: #343741; - background-color: #FFF; } - .kuiMenuButton--basic:focus { - color: #343741 !important; - /* 1 */ } - .kuiMenuButton--basic:hover, .kuiMenuButton--basic:active { - /* 2 */ - color: #343741 !important; - /* 1 */ - background-color: #F5F7FA; } - .kuiMenuButton--basic:disabled { - color: #98A2B3; - cursor: not-allowed; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--primary { - color: #FFF; - background-color: #006BB4; } - .kuiMenuButton--primary:focus { - color: #FFF !important; - /* 1 */ } - .kuiMenuButton--primary:hover, .kuiMenuButton--primary:active { - /* 2 */ - color: #FFF !important; - /* 1 */ - background-color: #004d81; } - .kuiMenuButton--primary:disabled { - background-color: #98A2B3; - cursor: not-allowed; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--danger { - color: #FFF; - background-color: #BD271E; } - .kuiMenuButton--danger:hover, .kuiMenuButton--danger:active { - /* 2 */ - color: #FFF !important; - /* 1 */ - background-color: #911e17; } - .kuiMenuButton--danger:disabled { - color: #FFF; - background-color: #98A2B3; - cursor: not-allowed; } - .kuiMenuButton--danger:focus { - z-index: 1; - /* 1 */ - outline: none !important; - /* 2 */ - -webkit-box-shadow: 0 0 0 1px #FFF, 0 0 0 2px #BD271E; - box-shadow: 0 0 0 1px #FFF, 0 0 0 2px #BD271E; - /* 3 */ } - -.kuiMenuButtonGroup { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; } - .kuiMenuButtonGroup .kuiMenuButton + .kuiMenuButton { - margin-left: 4px; } - -.kuiMenuButtonGroup--alignRight { - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; } - -.kuiModalOverlay { - position: fixed; - z-index: 1000; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - padding-bottom: 10vh; - background: rgba(255, 255, 255, 0.8); } - -.kuiModal { - -webkit-box-shadow: 0 12px 24px 0 rgba(65, 78, 101, 0.1), 0 6px 12px 0 rgba(65, 78, 101, 0.1), 0 4px 4px 0 rgba(65, 78, 101, 0.1), 0 2px 2px 0 rgba(65, 78, 101, 0.1); - box-shadow: 0 12px 24px 0 rgba(65, 78, 101, 0.1), 0 6px 12px 0 rgba(65, 78, 101, 0.1), 0 4px 4px 0 rgba(65, 78, 101, 0.1), 0 2px 2px 0 rgba(65, 78, 101, 0.1); - line-height: 1.5; - background-color: #FFF; - border: 1px solid #D3DAE6; - border-radius: 4px; - z-index: 1001; - -webkit-animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); - animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); } - -.kuiModal--confirmation { - width: 450px; - min-width: auto; } - -.kuiModalHeader { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; - padding: 10px; - padding-left: 20px; - border-bottom: 1px solid #D3DAE6; } - -.kuiModalHeader__title { - font-size: 20px; } - -.kuiModalHeaderCloseButton { - display: inline-block; - /* 1 */ - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - padding: 2px 5px; - border: 1px solid transparent; - color: #69707D; - background-color: transparent; - line-height: 1; - /* 2 */ - font-size: 20px; } - .kuiModalHeaderCloseButton:hover { - color: #343741; } - -.kuiModalBody { - padding: 20px; } - -.kuiModalBodyText { - font-size: 14px; } - -.kuiModalFooter { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; - padding: 20px; - padding-top: 10px; } - .kuiModalFooter > * + * { - margin-left: 5px; } - -@-webkit-keyframes kuiModal { - 0% { - opacity: 0; - -webkit-transform: translateY(32px); - transform: translateY(32px); } - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); } } - -@keyframes kuiModal { - 0% { - opacity: 0; - -webkit-transform: translateY(32px); - transform: translateY(32px); } - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); } } - /** * 1. Put 10px of space between each child. */ @@ -2090,107 +1811,6 @@ main { .kuiPanelBody { padding: 10px; } -.kuiPanelSimple { - -webkit-box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3), 0 1px 5px -2px rgba(152, 162, 179, 0.3); - box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3), 0 1px 5px -2px rgba(152, 162, 179, 0.3); - background-color: #FFF; - border: 1px solid #D3DAE6; - border-radius: 4px; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; } - .kuiPanelSimple.kuiPanelSimple--paddingSmall { - padding: 8px; } - .kuiPanelSimple.kuiPanelSimple--paddingMedium { - padding: 16px; } - .kuiPanelSimple.kuiPanelSimple--paddingLarge { - padding: 24px; } - .kuiPanelSimple.kuiPanelSimple--shadow { - -webkit-box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); - box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); } - .kuiPanelSimple.kuiPanelSimple--flexGrowZero { - -webkit-box-flex: 0; - -webkit-flex-grow: 0; - -ms-flex-positive: 0; - flex-grow: 0; } - -.kuiPopover { - display: inline-block; - position: relative; } - .kuiPopover.kuiPopover-isOpen .kuiPopover__panel { - opacity: 1; - visibility: visible; - margin-top: 8px; - pointer-events: auto; } - -.kuiPopover__panel { - position: absolute; - z-index: 2000; - top: 100%; - left: 50%; - -webkit-transform: translateX(-50%) translateY(8px) translateZ(0); - transform: translateX(-50%) translateY(8px) translateZ(0); - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; - transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; - -webkit-transform-origin: center top; - transform-origin: center top; - opacity: 0; - visibility: hidden; - pointer-events: none; - margin-top: 24px; } - .kuiPopover__panel:before { - position: absolute; - content: ""; - top: -16px; - height: 0; - width: 0; - left: 50%; - margin-left: -16px; - border-left: 16px solid transparent; - border-right: 16px solid transparent; - border-bottom: 16px solid #D3DAE6; } - .kuiPopover__panel:after { - position: absolute; - content: ""; - top: -15px; - right: 0; - height: 0; - left: 50%; - margin-left: -16px; - width: 0; - border-left: 16px solid transparent; - border-right: 16px solid transparent; - border-bottom: 16px solid #FFF; } - -.kuiPopover--withTitle .kuiPopover__panel:after { - border-bottom-color: #F5F7FA; } - -.kuiPopover--anchorLeft .kuiPopover__panel { - left: 0; - -webkit-transform: translateX(0%) translateY(8px) translateZ(0); - transform: translateX(0%) translateY(8px) translateZ(0); } - .kuiPopover--anchorLeft .kuiPopover__panel:before, .kuiPopover--anchorLeft .kuiPopover__panel:after { - right: auto; - left: 16px; - margin: 0; } - -.kuiPopover--anchorRight .kuiPopover__panel { - left: 100%; - -webkit-transform: translateX(-100%) translateY(8px) translateZ(0); - transform: translateX(-100%) translateY(8px) translateZ(0); } - .kuiPopover--anchorRight .kuiPopover__panel:before, .kuiPopover--anchorRight .kuiPopover__panel:after { - right: 16px; - left: auto; } - -.kuiPopoverTitle { - background-color: #F5F7FA; - border-bottom: 1px solid #D3DAE6; - padding: 12px; - font-size: 16px; } - .kuiEmptyTablePrompt { display: -webkit-box; display: -webkit-flex; @@ -2497,57 +2117,6 @@ main { background-color: transparent; border-bottom-color: transparent; } -/** - * 1. Allow container to determine font-size and line-height. - * 2. Override inherited Bootstrap styles. - */ -.kuiToggleButton { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - background-color: transparent; - border: none; - padding: 0; - font-size: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - color: #343741; } - .kuiToggleButton:focus { - color: #343741; } - .kuiToggleButton:active { - color: #006BB4 !important; - /* 2 */ } - .kuiToggleButton:hover:not(:disabled) { - color: #004d81 !important; - /* 2 */ - text-decoration: underline; } - .kuiToggleButton:disabled { - cursor: not-allowed; - opacity: .5; } - -/** - * 1. Make icon a consistent width so the text doesn't get pushed around as the icon changes - * between "expand" and "collapse". Use ems to be relative to inherited font-size. - */ -.kuiToggleButton__icon { - width: 0.8em; - /* 1 */ } - -.kuiTogglePanelHeader { - padding-bottom: 4px; - margin-bottom: 15px; - border-bottom: 1px solid #D3DAE6; - /** - * 1. Allow the user to click anywhere on the header, not just on the button text. - */ } - .kuiTogglePanelHeader .kuiToggleButton { - width: 100%; - /* 1 */ - text-align: left; - /* 1 */ } - .kuiToolBar { display: -webkit-box; display: -webkit-flex; diff --git a/packages/kbn-ui-framework/doc_site/src/services/routes/routes.js b/packages/kbn-ui-framework/doc_site/src/services/routes/routes.js index e7cd43d35e2b..32912d5eb9c8 100644 --- a/packages/kbn-ui-framework/doc_site/src/services/routes/routes.js +++ b/packages/kbn-ui-framework/doc_site/src/services/routes/routes.js @@ -19,184 +19,136 @@ import Slugify from '../string/slugify'; -import BarExample - from '../../views/bar/bar_example'; +import BarExample from '../../views/bar/bar_example'; -import ButtonExample - from '../../views/button/button_example'; +import ButtonExample from '../../views/button/button_example'; -import CollapseButtonExample - from '../../views/collapse_button/collapse_button_example'; +import CollapseButtonExample from '../../views/collapse_button/collapse_button_example'; -import ExpressionExample - from '../../views/expression/expression_example'; -import FormExample - from '../../views/form/form_example'; +import FormExample from '../../views/form/form_example'; -import FormLayoutExample - from '../../views/form_layout/form_layout_example'; +import FormLayoutExample from '../../views/form_layout/form_layout_example'; -import IconExample - from '../../views/icon/icon_example'; +import IconExample from '../../views/icon/icon_example'; -import InfoPanelExample - from '../../views/info_panel/info_panel_example'; +import InfoPanelExample from '../../views/info_panel/info_panel_example'; -import LinkExample - from '../../views/link/link_example'; +import LinkExample from '../../views/link/link_example'; -import LocalNavExample - from '../../views/local_nav/local_nav_example'; +import LocalNavExample from '../../views/local_nav/local_nav_example'; -import MenuExample - from '../../views/menu/menu_example'; +import PagerExample from '../../views/pager/pager_example'; -import MenuButtonExample - from '../../views/menu_button/menu_button_example'; +import PanelExample from '../../views/panel/panel_example'; -import ModalExample - from '../../views/modal/modal_example'; +import EmptyTablePromptExample from '../../views/empty_table_prompt/empty_table_prompt_example'; -import PagerExample - from '../../views/pager/pager_example'; +import StatusTextExample from '../../views/status_text/status_text_example'; -import PanelExample - from '../../views/panel/panel_example'; +import TableExample from '../../views/table/table_example'; -import PanelSimpleExample - from '../../views/panel_simple/panel_simple_example'; +import TabsExample from '../../views/tabs/tabs_example'; -import PopoverExample - from '../../views/popover/popover_example'; +import ToolBarExample from '../../views/tool_bar/tool_bar_example'; -import EmptyTablePromptExample - from '../../views/empty_table_prompt/empty_table_prompt_example'; +import TypographyExample from '../../views/typography/typography_example'; -import StatusTextExample - from '../../views/status_text/status_text_example'; +import VerticalRhythmExample from '../../views/vertical_rhythm/vertical_rhythm_example'; -import TableExample - from '../../views/table/table_example'; - -import TabsExample - from '../../views/tabs/tabs_example'; - -import ToggleButtonExample - from '../../views/toggle_button/toggle_button_example'; - -import ToolBarExample - from '../../views/tool_bar/tool_bar_example'; - -import TypographyExample - from '../../views/typography/typography_example'; - -import VerticalRhythmExample - from '../../views/vertical_rhythm/vertical_rhythm_example'; - -import ViewSandbox - from '../../views/view/view_sandbox'; +import ViewSandbox from '../../views/view/view_sandbox'; // Component route names should match the component name exactly. -const components = [{ - name: 'Bar', - component: BarExample, - hasReact: true, -}, { - name: 'Button', - component: ButtonExample, - hasReact: true, -}, { - name: 'CollapseButton', - component: CollapseButtonExample, - hasReact: true, -}, { - name: 'CollapseButton', - component: CollapseButtonExample, - hasReact: true, -}, { - name: 'EmptyTablePrompt', - component: EmptyTablePromptExample, - hasReact: true, -}, { - name: 'Expression', - component: ExpressionExample, - hasReact: true, -}, { - name: 'Form', - component: FormExample, -}, { - name: 'FormLayout', - component: FormLayoutExample, - hasReact: true, -}, { - name: 'Icon', - component: IconExample, -}, { - name: 'InfoPanel', - component: InfoPanelExample, -}, { - name: 'Link', - component: LinkExample, -}, { - name: 'LocalNav', - component: LocalNavExample, - hasReact: true, -}, { - name: 'Menu', - component: MenuExample, - hasReact: true, -}, { - name: 'MenuButton', - component: MenuButtonExample, -}, { - name: 'Modal', - component: ModalExample, - hasReact: true, -}, { - name: 'Pager', - component: PagerExample, - hasReact: true, -}, { - name: 'Panel', - component: PanelExample, -}, { - name: 'PanelSimple', - component: PanelSimpleExample, - hasReact: true, -}, { - name: 'Popover', - component: PopoverExample, - hasReact: true, -}, { - name: 'StatusText', - component: StatusTextExample, -}, { - name: 'Table', - component: TableExample, - hasReact: true, -}, { - name: 'Tabs', - component: TabsExample, - hasReact: true, -}, { - name: 'ToggleButton', - component: ToggleButtonExample, -}, { - name: 'ToolBar', - component: ToolBarExample, - hasReact: true, -}, { - name: 'Typography', - component: TypographyExample, -}, { - name: 'VerticalRhythm', - component: VerticalRhythmExample, -}]; - -const sandboxes = [{ - name: 'View', - component: ViewSandbox, -}]; +const components = [ + { + name: 'Bar', + component: BarExample, + hasReact: true, + }, + { + name: 'Button', + component: ButtonExample, + hasReact: true, + }, + { + name: 'CollapseButton', + component: CollapseButtonExample, + hasReact: true, + }, + { + name: 'EmptyTablePrompt', + component: EmptyTablePromptExample, + hasReact: true, + }, + { + name: 'Form', + component: FormExample, + }, + { + name: 'FormLayout', + component: FormLayoutExample, + hasReact: true, + }, + { + name: 'Icon', + component: IconExample, + }, + { + name: 'InfoPanel', + component: InfoPanelExample, + }, + { + name: 'Link', + component: LinkExample, + }, + { + name: 'LocalNav', + component: LocalNavExample, + hasReact: true, + }, + { + name: 'Pager', + component: PagerExample, + hasReact: true, + }, + { + name: 'Panel', + component: PanelExample, + }, + { + name: 'StatusText', + component: StatusTextExample, + }, + { + name: 'Table', + component: TableExample, + hasReact: true, + }, + { + name: 'Tabs', + component: TabsExample, + hasReact: true, + }, + { + name: 'ToolBar', + component: ToolBarExample, + hasReact: true, + }, + { + name: 'Typography', + component: TypographyExample, + }, + { + name: 'VerticalRhythm', + component: VerticalRhythmExample, + }, +]; + +const sandboxes = [ + { + name: 'View', + component: ViewSandbox, + }, +]; const allRoutes = components.concat(sandboxes); diff --git a/packages/kbn-ui-framework/doc_site/src/views/expression/expression.js b/packages/kbn-ui-framework/doc_site/src/views/expression/expression.js deleted file mode 100644 index 754b083c72cd..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/expression/expression.js +++ /dev/null @@ -1,220 +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 PropTypes from 'prop-types'; -import { - KuiExpression, - KuiExpressionButton, - KuiFieldGroup, - KuiFieldGroupSection, - KuiPopover, - KuiPopoverTitle, -} from '../../../../components'; - - -class KuiExpressionItemExample extends React.Component { - constructor(props) { - super(props); - - this.state = { - example1: { - isOpen: false, - value: 'count()' - }, - example2: { - object: 'A', - value: '100', - description: 'Is above' - }, - }; - } - - openExample1 = () => { - this.setState({ - example1: { - ...this.state.example1, - isOpen: true, - }, - example2: { - ...this.state.example2, - isOpen: false, - }, - }); - }; - - closeExample1 = () => { - this.setState({ - example1: { - ...this.state.example1, - isOpen: false, - }, - }); - }; - - openExample2 = () => { - this.setState({ - example1: { - ...this.state.example1, - isOpen: false, - }, - example2: { - ...this.state.example2, - isOpen: true, - }, - }); - }; - - closeExample2 = () => { - this.setState({ - example2: { - ...this.state.example2, - isOpen: false, - }, - }); - }; - - changeExample1 = (event) => { - this.setState({ example1: { ...this.state.example1, value: event.target.value } }); - } - - changeExample2Object = (event) => { - this.setState({ example2: { ...this.state.example2, object: event.target.value } }); - } - - changeExample2Value = (event) => { - this.setState({ example2: { ...this.state.example2, value: event.target.value } }); - } - - changeExample2Description = (event) => { - this.setState({ example2: { ...this.state.example2, description: event.target.value } }); - } - - render() { - // Rise the popovers above GuidePageSideNav - const popoverStyle = { zIndex: '200' }; - - return ( - - - - )} - isOpen={this.state.example1.isOpen} - closePopover={this.closeExample1} - panelPaddingSize="none" - withTitle - > - {this.getPopover1(popoverStyle)} - - - - - - )} - isOpen={this.state.example2.isOpen} - closePopover={this.closeExample2} - panelPaddingSize="none" - withTitle - anchorPosition="left" - > - {this.getPopover2(popoverStyle)} - - - - ); - } - - getPopover1(popoverStyle) { - return ( -

- When - - - -
- ); - } - - getPopover2(popoverStyle) { - return ( -
- {this.state.example2.description} - - - - - - - -
- ); - } -} - -KuiExpressionItemExample.propTypes = { - defaultActiveButton: PropTypes.string.isRequired -}; - -export default KuiExpressionItemExample; diff --git a/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js b/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js deleted file mode 100644 index c85c8305d685..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.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. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; -import { renderToHtml } from '../../services'; - -import { - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import Expression from './expression'; -import expressionSource from '!!raw-loader!./expression'; -const expressionHtml = renderToHtml(Expression, { defaultActiveButton: 'example2' }); - -export default props => ( - - - - ExpressionButtons allow you to compress a complicated form into a small space. - - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu/menu.js b/packages/kbn-ui-framework/doc_site/src/views/menu/menu.js deleted file mode 100644 index ca9b6ccc1925..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu/menu.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. - */ - -import React from 'react'; - -import { - KuiMenu, - KuiMenuItem, -} from '../../../../components'; - -export default () => ( -
- - -

Item A

-
- - -

Item B

-
- - -

Item C

-
-
-
-); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_contained.js b/packages/kbn-ui-framework/doc_site/src/views/menu/menu_contained.js deleted file mode 100644 index 0ab679613942..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_contained.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. - */ - -import React from 'react'; - -import { - KuiMenu, - KuiMenuItem, -} from '../../../../components'; - -export default () => ( -
- - -

Item A

-
- - -

Item B

-
- - -

Item C

-
-
-
-); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js b/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js deleted file mode 100644 index 8e31432a1ccc..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js +++ /dev/null @@ -1,73 +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. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; - -import { renderToHtml } from '../../services'; - -import { - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, -} from '../../components'; - -import Menu from './menu'; -import menuSource from '!!raw-loader!./menu'; -const menuHtml = renderToHtml(Menu); - -import MenuContained from './menu_contained'; -import menuContainedSource from '!!raw-loader!./menu_contained'; -const menuContainedHtml = renderToHtml(MenuContained); - -export default props => ( - - - - - - - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_basic.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_basic.html deleted file mode 100644 index 580701220dfe..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_basic.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_danger.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_danger.html deleted file mode 100644 index 07463ba68ef5..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_danger.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_elements.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_elements.html deleted file mode 100644 index b8090e21c299..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_elements.html +++ /dev/null @@ -1,17 +0,0 @@ - - -  - - - -  - - - Anchor element - diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js deleted file mode 100644 index 6677a7b29e52..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js +++ /dev/null @@ -1,119 +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 { - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import basicHtml from './menu_button_basic.html'; -import primaryHtml from './menu_button_primary.html'; -import dangerHtml from './menu_button_danger.html'; -import withIconHtml from './menu_button_with_icon.html'; -import groupHtml from './menu_button_group.html'; -import elementsHtml from './menu_button_elements.html'; - -export default props => ( - - - - - - - - - - - - - - - - You can use a MenuButton with an Icon, with or without text. - - - - - - - - - - - - You can create a MenuButton using a button element, link, or input[type=“submit”]. - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_group.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_group.html deleted file mode 100644 index 05d959d730dc..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_group.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - - -
- -
- -
- -
diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_primary.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_primary.html deleted file mode 100644 index 67bed3fcbf81..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_primary.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_with_icon.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_with_icon.html deleted file mode 100644 index 9ef05dca41a1..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_with_icon.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- - - -
- - diff --git a/packages/kbn-ui-framework/doc_site/src/views/modal/confirm_modal.js b/packages/kbn-ui-framework/doc_site/src/views/modal/confirm_modal.js deleted file mode 100644 index 8ef7e88fe149..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/modal/confirm_modal.js +++ /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 React, { - Component, -} from 'react'; - -import { - KuiButton, - KuiConfirmModal, - KuiModalOverlay, - KUI_MODAL_CONFIRM_BUTTON, -} from '../../../../components'; - -export class ConfirmModalExample extends Component { - constructor(props) { - super(props); - - this.state = { - isModalVisible: false, - }; - - this.closeModal = this.closeModal.bind(this); - this.showModal = this.showModal.bind(this); - } - - closeModal() { - this.setState({ isModalVisible: false }); - } - - showModal() { - this.setState({ isModalVisible: true }); - } - - render() { - let modal; - - if (this.state.isModalVisible) { - modal = ( - - -

You’re about to do something.

-

Are you sure you want to do this?

-
-
- ); - } - - return ( -
- - Show ConfirmModal - - - {modal} -
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/modal/modal.js b/packages/kbn-ui-framework/doc_site/src/views/modal/modal.js deleted file mode 100644 index 52fd031ccf42..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/modal/modal.js +++ /dev/null @@ -1,102 +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, { - Component, -} from 'react'; - -import { - KuiButton, - KuiModal, - KuiModalBody, - KuiModalFooter, - KuiModalHeader, - KuiModalHeaderTitle, - KuiModalOverlay, -} from '../../../../components'; - -export class ModalExample extends Component { - constructor(props) { - super(props); - - this.state = { - isModalVisible: false, - }; - } - - closeModal = () => { - this.setState({ isModalVisible: false }); - }; - - showModal = () => { - this.setState({ isModalVisible: true }); - }; - - render() { - let modal; - - if (this.state.isModalVisible) { - modal = ( - - - - - Modal - - - - -

- You can put anything you want in here! -

-
- - - - Cancel - - - - Save - - -
-
- ); - } - return ( -
- - Show Modal - - - {modal} -
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js b/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js deleted file mode 100644 index b21ee91ddb19..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js +++ /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. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; - -import { renderToHtml } from '../../services'; - -import { - GuideCode, - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import { ModalExample } from './modal'; -import modalSource from '!!raw-loader!./modal'; // eslint-disable-line import/default -const modalHtml = renderToHtml(ModalExample); - -import { ConfirmModalExample } from './confirm_modal'; -import confirmModalSource from '!!raw-loader!./confirm_modal'; // eslint-disable-line import/default -const confirmModalHtml = renderToHtml(ConfirmModalExample); - -export default props => ( - - - - - Use a KuiModal to temporarily escape the current UX and create - another UX within it. - - - - - - - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple.js b/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple.js deleted file mode 100644 index 376e60335833..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple.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 React from 'react'; - -import { - KuiPanelSimple, -} from '../../../../components'; - -export default () => ( -
- - sizePadding="none" - - -
- - - sizePadding="s" - - -
- - - sizePadding="m" - - -
- - - sizePadding="l" - - -
- - - sizePadding="l", hasShadow - -
-); diff --git a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js b/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js deleted file mode 100644 index 3f7ffa2eec08..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.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. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; - -import { Link } from 'react-router'; - -import { renderToHtml } from '../../services'; - -import { - GuideCode, - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import PanelSimple from './panel_simple'; -import panelSimpleSource from '!!raw-loader!./panel_simple'; -const panelSimpleHtml = renderToHtml(PanelSimple); - -export default props => ( - - - - PanelSimple is a simple wrapper component to add - depth to a contained layout. It it commonly used as a base for - other larger components like Popover. - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover.js deleted file mode 100644 index 59e47ef348b7..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover.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 React, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiButton, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - const button = ( - - Show popover - - ); - - return ( - -
Popover content that’s wider than the default width
-
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_anchor_position.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_anchor_position.js deleted file mode 100644 index 54e0f97542ac..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_anchor_position.js +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiButton, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen1: false, - isPopoverOpen2: false, - }; - } - - onButtonClick1() { - this.setState({ - isPopoverOpen1: !this.state.isPopoverOpen1, - }); - } - - closePopover1() { - this.setState({ - isPopoverOpen1: false, - }); - } - - onButtonClick2() { - this.setState({ - isPopoverOpen2: !this.state.isPopoverOpen2, - }); - } - - closePopover2() { - this.setState({ - isPopoverOpen2: false, - }); - } - - render() { - return ( -
- - Popover anchored to the right. - - )} - isOpen={this.state.isPopoverOpen1} - closePopover={this.closePopover1.bind(this)} - anchorPosition="right" - > - Popover content - - -   - - - Popover anchored to the left. - - )} - isOpen={this.state.isPopoverOpen2} - closePopover={this.closePopover2.bind(this)} - anchorPosition="left" - > - Popover content - -
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_body_class_name.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_body_class_name.js deleted file mode 100644 index e347c5a32118..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_body_class_name.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 React, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiButton -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - return ( - - Custom class - - )} - isOpen={this.state.isPopoverOpen} - closePopover={this.closePopover.bind(this)} - bodyClassName="yourClassNameHere" - > - It’s hard to tell but there’s a custom class on this element - - ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js deleted file mode 100644 index 90317ad5c56c..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js +++ /dev/null @@ -1,160 +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. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; - -import { renderToHtml } from '../../services'; - -import { - GuideCode, - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import Popover from './popover'; -import popoverSource from '!!raw-loader!./popover'; -const popoverHtml = renderToHtml(Popover); - -import TrapFocus from './trap_focus'; -import trapFocusSource from '!!raw-loader!./trap_focus'; -const trapFocusHtml = renderToHtml(TrapFocus); - -import PopoverAnchorPosition from './popover_anchor_position'; -import popoverAnchorPositionSource from '!!raw-loader!./popover_anchor_position'; -const popoverAnchorPositionHtml = renderToHtml(PopoverAnchorPosition); - -import PopoverPanelClassName from './popover_panel_class_name'; -import popoverPanelClassNameSource from '!!raw-loader!./popover_panel_class_name'; -const popoverPanelClassNameHtml = renderToHtml(PopoverPanelClassName); - -import PopoverWithTitle from './popover_with_title'; -import popoverWithTitleSource from '!!raw-loader!./popover_with_title'; -const popoverWithTitleHtml = renderToHtml(PopoverWithTitle); - -export default props => ( - - - - Use the Popover component to hide controls or options behind a clickable element. - - - - - - - - - - If the Popover should be responsible for trapping the focus within itself (as opposed - to a child component), then you should set ownFocus. - - - - - - - - - - Popovers often have need for titling. This can be applied through - a prop or used separately as its own component - KuiPopoverTitle nested somewhere in the child - prop. - - - - - - - - - - The alignment and arrow on your popover can be set with - the anchorPosition prop. - - - - - - - - - - Use the panelPaddingSize prop to adjust the padding - on the panel within the panel. Use the panelClassName - prop to pass a custom class to the panel. - inside a popover. - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_panel_class_name.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_panel_class_name.js deleted file mode 100644 index c3b98fc6ca3d..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_panel_class_name.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 React, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiButton, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - return ( - - Turn padding off and apply a custom class - - )} - isOpen={this.state.isPopoverOpen} - closePopover={this.closePopover.bind(this)} - panelClassName="yourClassNameHere" - panelPaddingSize="none" - > - This should have no padding, and if you inspect, also a custom class. - - ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_with_title.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_with_title.js deleted file mode 100644 index ff25a54e1d60..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_with_title.js +++ /dev/null @@ -1,79 +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, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiPopoverTitle, - KuiButton, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - const button = ( - - Show popover with Title - - ); - - return ( - -
- Hello, I’m a popover title -

- Popover content that’s wider than the default width -

-
-
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/trap_focus.js b/packages/kbn-ui-framework/doc_site/src/views/popover/trap_focus.js deleted file mode 100644 index b034da504f3d..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/trap_focus.js +++ /dev/null @@ -1,96 +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, { - Component, -} from 'react'; - -import { - KuiButton, - KuiFieldGroup, - KuiFieldGroupSection, - KuiPopover, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - const button = ( - - Show popover - - ); - - return ( - -
-
- - -
-
- -
- - - - - - -
- -
- Save -
-
- - ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/table/table_with_menu_buttons.js b/packages/kbn-ui-framework/doc_site/src/views/table/table_with_menu_buttons.js index 4b1b06387686..49b4952de63a 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/table/table_with_menu_buttons.js +++ b/packages/kbn-ui-framework/doc_site/src/views/table/table_with_menu_buttons.js @@ -28,86 +28,29 @@ import { KuiTableBody, } from '../../../../components'; -import { - RIGHT_ALIGNMENT -} from '../../../../src/services'; - export function TableWithMenuButtons() { return ( - - Reminder - - - A - - - B - - - C - - - Actions - + Reminder + A + B + C - - Core temperature critical - - - A - - - B - - - C - - -
- - - -
-
+ Core temperature critical + A + B + C
- - Time for your snack - - - A - - - B - - - C - - -
- - - -
-
+ Time for your snack + A + B + C
diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.html b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.html deleted file mode 100644 index 33ae80390fe3..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.js b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.js deleted file mode 100644 index 90fec3871cdc..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable */ - -let isButtonCollapsed = true; -const $toggleButton = $('[data-id="toggleButton"]'); -const $toggleButtonIcon = $('[data-id="toggleButtonIcon"]'); - -$toggleButton.on('click', () => { - isButtonCollapsed = !isButtonCollapsed; - - if (isButtonCollapsed) { - $toggleButtonIcon.addClass('fa-caret-right'); - $toggleButtonIcon.removeClass('fa-caret-down'); - } else { - $toggleButtonIcon.removeClass('fa-caret-right'); - $toggleButtonIcon.addClass('fa-caret-down'); - } -}); diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_disabled.html b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_disabled.html deleted file mode 100644 index 0b3790f5084e..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_disabled.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js deleted file mode 100644 index 6fd3a816e4fc..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js +++ /dev/null @@ -1,86 +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 { - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import { - Link, -} from 'react-router'; - -import toggleButtonHtml from './toggle_button.html'; -import toggleButtonJs from 'raw-loader!./toggle_button.js'; -import toggleButtonDisabledHtml from './toggle_button_disabled.html'; -import togglePanelHtml from './toggle_panel.html'; -import togglePanelJs from 'raw-loader!./toggle_panel.js'; - -export default props => ( - - - - You can use this button to reveal and hide content. For a complete example - on how to make an collapsable panel proper accessible, read - the CollapseButton documentation. - - - - - - - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.html b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.html deleted file mode 100644 index 561a307d4c99..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
- -
- -
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis egestas dictum enim non lobortis. Curabitur vel viverra metus. Ut non dignissim neque. Nulla metus lorem, maximus et vehicula vel, pharetra vitae sem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam mi risus, varius in elit a, scelerisque blandit dolor. Donec ut leo mi. Duis ac tincidunt urna. Sed finibus eros odio, vitae euismod turpis ullamcorper pulvinar. Sed in ipsum at magna euismod tristique. Donec eget orci blandit, convallis odio sed, hendrerit dui. Aenean augue nibh, hendrerit sit amet aliquet et, efficitur sit amet nulla. Vivamus placerat pulvinar ipsum, dignissim sodales libero pulvinar sit amet. Maecenas pellentesque neque at quam varius aliquam. Ut at pretium augue, a pellentesque neque.

-
-
diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.js b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.js deleted file mode 100644 index 897662a814cf..000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable */ - -let isButtonCollapsed = true; -const $togglePanelButton = $('[data-id="togglePanelButton"]'); -const $togglePanelButtonIcon = $('[data-id="togglePanelButtonIcon"]'); -const $togglePanelContent = $('[data-id="togglePanelContent"]'); - -$togglePanelButton.on('click', () => { - isButtonCollapsed = !isButtonCollapsed; - - $togglePanelButton.attr('aria-expanded', !isButtonCollapsed); - - if (isButtonCollapsed) { - $togglePanelButtonIcon.addClass('fa-caret-right'); - $togglePanelButtonIcon.removeClass('fa-caret-down'); - $togglePanelContent.hide(); - } else { - $togglePanelButtonIcon.removeClass('fa-caret-right'); - $togglePanelButtonIcon.addClass('fa-caret-down'); - $togglePanelContent.show(); - } -}); - -$togglePanelContent.hide(); diff --git a/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression.test.js.snap b/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression.test.js.snap deleted file mode 100644 index 6d83f6aad8da..000000000000 --- a/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression.test.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiExpression Props children is rendered 1`] = ` -
- some expression -
-`; - -exports[`KuiExpression renders 1`] = ` -
-`; diff --git a/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression_button.test.js.snap b/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression_button.test.js.snap deleted file mode 100644 index c3bbae52bd65..000000000000 --- a/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression_button.test.js.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiExpressionButton Props isActive false renders inactive 1`] = ` - -`; - -exports[`KuiExpressionButton Props isActive true renders active 1`] = ` - -`; - -exports[`KuiExpressionButton renders 1`] = ` - -`; diff --git a/packages/kbn-ui-framework/src/components/expression/_expression.scss b/packages/kbn-ui-framework/src/components/expression/_expression.scss deleted file mode 100644 index bb01f5fa8359..000000000000 --- a/packages/kbn-ui-framework/src/components/expression/_expression.scss +++ /dev/null @@ -1,27 +0,0 @@ -.kuiExpression { - padding: 20px; - white-space: nowrap; -} - -.kuiExpressionButton { - background-color: transparent; - padding: 5px 0px; - border: none; - border-bottom: dotted 2px $kuiBorderColor; - font-size: $kuiFontSize; - cursor: pointer; -} - -.kuiExpressionButton__description { - color: $expressionColorHighlight; - text-transform: uppercase; -} - -.kuiExpressionButton__value { - color: $kuiTextColor; - text-transform: lowercase; -} - -.kuiExpressionButton-isActive { - border-bottom: solid 2px $expressionColorHighlight; -} diff --git a/packages/kbn-ui-framework/src/components/expression/_index.scss b/packages/kbn-ui-framework/src/components/expression/_index.scss deleted file mode 100644 index 7d806e314a27..000000000000 --- a/packages/kbn-ui-framework/src/components/expression/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -$expressionColorHighlight: $euiColorSecondary; - -@import "expression"; diff --git a/packages/kbn-ui-framework/src/components/expression/expression.js b/packages/kbn-ui-framework/src/components/expression/expression.js deleted file mode 100644 index 4464a1b742ba..000000000000 --- a/packages/kbn-ui-framework/src/components/expression/expression.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 React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export const KuiExpression = ({ - children, - className, - ...rest -}) => { - const classes = classNames('kuiExpression', className); - - return ( -
- {children} -
- ); -}; - -KuiExpression.propTypes = { - children: PropTypes.node, - className: PropTypes.string -}; diff --git a/packages/kbn-ui-framework/src/components/expression/expression.test.js b/packages/kbn-ui-framework/src/components/expression/expression.test.js deleted file mode 100644 index 5043dff09aa3..000000000000 --- a/packages/kbn-ui-framework/src/components/expression/expression.test.js +++ /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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiExpression, -} from './expression'; - -describe('KuiExpression', () => { - test('renders', () => { - const component = ( - - ); - - expect(render(component)).toMatchSnapshot(); - }); - - describe('Props', () => { - describe('children', () => { - test('is rendered', () => { - const component = render( - - some expression - - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/expression/expression_button.js b/packages/kbn-ui-framework/src/components/expression/expression_button.js deleted file mode 100644 index fd63469c1eff..000000000000 --- a/packages/kbn-ui-framework/src/components/expression/expression_button.js +++ /dev/null @@ -1,58 +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 PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export const KuiExpressionButton = ({ - className, - description, - buttonValue, - isActive, - onClick, - ...rest -}) => { - const classes = classNames('kuiExpressionButton', className, { - 'kuiExpressionButton-isActive': isActive - }); - - return ( - - ); -}; - -KuiExpressionButton.propTypes = { - className: PropTypes.string, - description: PropTypes.string.isRequired, - buttonValue: PropTypes.string.isRequired, - isActive: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -}; - -KuiExpressionButton.defaultProps = { - isActive: false, -}; diff --git a/packages/kbn-ui-framework/src/components/expression/expression_button.test.js b/packages/kbn-ui-framework/src/components/expression/expression_button.test.js deleted file mode 100644 index 69149b947662..000000000000 --- a/packages/kbn-ui-framework/src/components/expression/expression_button.test.js +++ /dev/null @@ -1,93 +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 { render, shallow } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; -import sinon from 'sinon'; - -import { - KuiExpressionButton, -} from './expression_button'; - -describe('KuiExpressionButton', () => { - test('renders', () => { - const component = ( - {}} - {...requiredProps} - /> - ); - - expect(render(component)).toMatchSnapshot(); - }); - - describe('Props', () => { - describe('isActive', () => { - test('true renders active', () => { - const component = ( - {}} - /> - ); - - expect(render(component)).toMatchSnapshot(); - }); - - test('false renders inactive', () => { - const component = ( - {}} - /> - ); - - expect(render(component)).toMatchSnapshot(); - }); - }); - - describe('onClick', () => { - test('is called when the button is clicked', () => { - const onClickHandler = sinon.spy(); - - const button = shallow( - - ); - - button.simulate('click'); - - sinon.assert.calledOnce(onClickHandler); - }); - }); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/expression/index.js b/packages/kbn-ui-framework/src/components/expression/index.js deleted file mode 100644 index 387e42553a62..000000000000 --- a/packages/kbn-ui-framework/src/components/expression/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 { KuiExpression } from './expression'; -export { KuiExpressionButton } from './expression_button'; diff --git a/packages/kbn-ui-framework/src/components/index.js b/packages/kbn-ui-framework/src/components/index.js index d1a94257abc9..7bc0f4aaf2cf 100644 --- a/packages/kbn-ui-framework/src/components/index.js +++ b/packages/kbn-ui-framework/src/components/index.js @@ -17,22 +17,11 @@ * under the License. */ -export { - KuiBar, - KuiBarSection, -} from './bar'; +export { KuiBar, KuiBarSection } from './bar'; -export { - KuiButton, - KuiButtonGroup, - KuiButtonIcon, - KuiLinkButton, - KuiSubmitButton, -} from './button'; +export { KuiButton, KuiButtonGroup, KuiButtonIcon, KuiLinkButton, KuiSubmitButton } from './button'; -export { - KuiCollapseButton, -} from './collapse_button'; +export { KuiCollapseButton } from './collapse_button'; export { KuiEmptyTablePrompt, @@ -40,15 +29,7 @@ export { KuiEmptyTablePromptPanel, } from './empty_table_prompt'; -export { - KuiExpression, - KuiExpressionButton, -} from './expression'; - -export { - KuiFieldGroup, - KuiFieldGroupSection, -} from './form_layout'; +export { KuiFieldGroup, KuiFieldGroupSection } from './form_layout'; export { KuiLabel, @@ -68,45 +49,9 @@ export { KuiLocalTitle, } from './local_nav'; -export { - KuiMenu, - KuiMenuItem, -} from './menu'; - -export { - KUI_MODAL_CANCEL_BUTTON, - KUI_MODAL_CONFIRM_BUTTON, - KuiConfirmModal, - KuiModal, - KuiModalBody, - KuiModalFooter, - KuiModalHeader, - KuiModalHeaderTitle, - KuiModalOverlay, -} from './modal'; - -export { - KuiOutsideClickDetector, -} from './outside_click_detector'; +export { KuiPager, KuiPagerButtonGroup } from './pager'; -export { - KuiPager, - KuiPagerButtonGroup, -} from './pager'; - -export { - KuiPanelSimple, -} from './panel_simple'; - -export { - KuiPopover, - KuiPopoverTitle, -} from './popover'; - -export { - KuiTabs, - KuiTab -} from './tabs'; +export { KuiTabs, KuiTab } from './tabs'; export { KuiTable, @@ -123,7 +68,7 @@ export { KuiListingTableCreateButton, KuiListingTableDeleteButton, KuiListingTableNoMatchesPrompt, - KuiListingTableLoadingPrompt + KuiListingTableLoadingPrompt, } from './table'; export { @@ -132,5 +77,5 @@ export { KuiToolBarFooter, KuiToolBarSection, KuiToolBarFooterSection, - KuiToolBarText + KuiToolBarText, } from './tool_bar'; diff --git a/packages/kbn-ui-framework/src/components/index.scss b/packages/kbn-ui-framework/src/components/index.scss index 084e7045a393..5e12ef30c8c9 100644 --- a/packages/kbn-ui-framework/src/components/index.scss +++ b/packages/kbn-ui-framework/src/components/index.scss @@ -12,30 +12,23 @@ // When possible, if making changes to those legacy components, please think about // instead adding them to this library and deprecating that dependency. -@import "bar/index"; -@import "button/index"; -@import "collapse_button/index"; -@import "expression/index"; -@import "form/index"; -@import "form_layout/index"; -@import "icon/index"; -@import "info_panel/index"; -@import "link/index"; -@import "local_nav/index"; -@import "menu/index"; -@import "menu_button/index"; -@import "modal/index"; -@import "pager/index"; -@import "panel/index"; -@import "panel_simple/index"; -@import "popover/index"; -@import "empty_table_prompt/index"; -@import "status_text/index"; -@import "table/index"; -@import "table_info/index"; -@import "tabs/index"; -@import "toggle_button/index"; -@import "tool_bar/index"; -@import "typography/index"; -@import "vertical_rhythm/index"; -@import "view/index"; +@import 'bar/index'; +@import 'button/index'; +@import 'collapse_button/index'; +@import 'form/index'; +@import 'form_layout/index'; +@import 'icon/index'; +@import 'info_panel/index'; +@import 'link/index'; +@import 'local_nav/index'; +@import 'pager/index'; +@import 'panel/index'; +@import 'empty_table_prompt/index'; +@import 'status_text/index'; +@import 'table/index'; +@import 'table_info/index'; +@import 'tabs/index'; +@import 'tool_bar/index'; +@import 'typography/index'; +@import 'vertical_rhythm/index'; +@import 'view/index'; diff --git a/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu.test.js.snap b/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu.test.js.snap deleted file mode 100644 index d33aa9635e93..000000000000 --- a/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu.test.js.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`contained prop 1`] = ` -
    - children -
-`; - -exports[`renders KuiMenu 1`] = ` -
    - children -
-`; diff --git a/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu_item.test.js.snap b/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu_item.test.js.snap deleted file mode 100644 index a7e54006a6dc..000000000000 --- a/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu_item.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiMenuItem 1`] = ` -
  • - children -
  • -`; diff --git a/packages/kbn-ui-framework/src/components/menu/_index.scss b/packages/kbn-ui-framework/src/components/menu/_index.scss deleted file mode 100644 index 1884d62ee83b..000000000000 --- a/packages/kbn-ui-framework/src/components/menu/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "menu"; diff --git a/packages/kbn-ui-framework/src/components/menu/_menu.scss b/packages/kbn-ui-framework/src/components/menu/_menu.scss deleted file mode 100644 index 812a216153a2..000000000000 --- a/packages/kbn-ui-framework/src/components/menu/_menu.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 1. Allow class to be applied to `ul` and `ol` elements - */ -.kuiMenu { - padding-left: 0; /* 1 */ -} - -.kuiMenu--contained { - border: $kuiBorderThin; - - .kuiMenuItem { - padding: 6px 10px; - } -} - -/** - * 1. Allow class to be applied to `li` elements - */ -.kuiMenuItem { - list-style: none; /* 1 */ - padding: 6px 0; - - & + & { - border-top: $kuiBorderThin; - } -} diff --git a/packages/kbn-ui-framework/src/components/menu/index.js b/packages/kbn-ui-framework/src/components/menu/index.js deleted file mode 100644 index 54a6e4e1d82d..000000000000 --- a/packages/kbn-ui-framework/src/components/menu/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 { KuiMenu } from './menu'; -export { KuiMenuItem } from './menu_item'; diff --git a/packages/kbn-ui-framework/src/components/menu/menu.js b/packages/kbn-ui-framework/src/components/menu/menu.js deleted file mode 100644 index e0707293cca6..000000000000 --- a/packages/kbn-ui-framework/src/components/menu/menu.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. - */ - -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; - -export const KuiMenu = ({ - contained, - className, - children, - ...rest -}) => { - const classes = classNames('kuiMenu', className, { - 'kuiMenu--contained': contained - }); - - return ( -
      - {children} -
    - ); -}; - -KuiMenu.propTypes = { - contained: PropTypes.bool, - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/menu/menu.test.js b/packages/kbn-ui-framework/src/components/menu/menu.test.js deleted file mode 100644 index 1ea1e5044faa..000000000000 --- a/packages/kbn-ui-framework/src/components/menu/menu.test.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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiMenu, -} from './menu'; - -test('renders KuiMenu', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); - -test('contained prop', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/menu/menu_item.js b/packages/kbn-ui-framework/src/components/menu/menu_item.js deleted file mode 100644 index b81d2053f75b..000000000000 --- a/packages/kbn-ui-framework/src/components/menu/menu_item.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. - */ - -import PropTypes from 'prop-types'; -import React from 'react'; - -import classNames from 'classnames'; - -export const KuiMenuItem = ({ - className, - children, - ...rest -}) => { - return ( -
  • - {children} -
  • - ); -}; - -KuiMenuItem.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/menu/menu_item.test.js b/packages/kbn-ui-framework/src/components/menu/menu_item.test.js deleted file mode 100644 index e06c11077784..000000000000 --- a/packages/kbn-ui-framework/src/components/menu/menu_item.test.js +++ /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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiMenuItem, -} from './menu_item'; - -test('renders KuiMenuItem', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/menu_button/_index.scss b/packages/kbn-ui-framework/src/components/menu_button/_index.scss deleted file mode 100644 index 1548f7e600be..000000000000 --- a/packages/kbn-ui-framework/src/components/menu_button/_index.scss +++ /dev/null @@ -1,20 +0,0 @@ -$menuButtonFontSize: 12px; - -$menuButtonBasicTextColor: $euiTextColor; -$menuButtonBasicBackgroundColor: $euiColorEmptyShade; -$menuButtonBasicHoverBackgroundColor: $euiColorLightestShade; -$menuButtonBasicDisabledTextColor: $euiColorMediumShade; -$menuButtonPrimaryTextColor: $euiColorGhost; -$menuButtonPrimaryBackgroundColor: $euiColorPrimary; -$menuButtonPrimaryHoverBackgroundColor: darken($euiColorPrimary, 10%); -$menuButtonPrimaryDisabledBackgroundColor: $euiColorMediumShade; - -$menuButtonDangerTextColor: $euiColorGhost; -$menuButtonDangerBackgroundColor: $euiColorDanger; -$menuButtonDangerHoverTextColor: $euiColorGhost; -$menuButtonDangerHoverBackgroundColor: darken($euiColorDanger, 10%); -$menuButtonDangerDisabledBackgroundColor: $euiColorMediumShade; -$menuButtonDangerHoverDisabledTextColor: $euiColorGhost; - -@import "menu_button"; -@import "menu_button_group"; diff --git a/packages/kbn-ui-framework/src/components/menu_button/_menu_button.scss b/packages/kbn-ui-framework/src/components/menu_button/_menu_button.scss deleted file mode 100644 index 2b9a8048746e..000000000000 --- a/packages/kbn-ui-framework/src/components/menu_button/_menu_button.scss +++ /dev/null @@ -1,118 +0,0 @@ -/** - * 1. Setting to inline-block guarantees the same height when applied to both - * button elements and anchor tags. - * 2. Disable for Angular. - * 3. Make the button just tall enough to fit inside an Option Layout. - */ -.kuiMenuButton { - display: inline-block; /* 1 */ - appearance: none; - cursor: pointer; - padding: 2px 10px; /* 3 */ - font-size: $menuButtonFontSize; - font-weight: $kuiFontWeightRegular; - line-height: $kuiLineHeight; - text-decoration: none; - border: none; - border-radius: $kuiBorderRadius; - - &:disabled { - cursor: default; - pointer-events: none; /* 2 */ - } - - &:active:enabled { - transform: translateY(1px); - } - - &:focus { - @include focus; - } -} - -.kuiMenuButton--iconText { - .kuiMenuButton__icon { - &:first-child { - margin-right: 4px; - } - - &:last-child { - margin-left: 4px; - } - } -} - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--basic { - color: $menuButtonBasicTextColor; - background-color: $menuButtonBasicBackgroundColor; - - // Goes before hover, so that hover can override it. - &:focus { - color: $menuButtonBasicTextColor !important; /* 1 */ - } - - &:hover, /* 2 */ - &:active { /* 2 */ - color: $menuButtonBasicTextColor !important; /* 1 */ - background-color: $menuButtonBasicHoverBackgroundColor; - } - - &:disabled { - color: $menuButtonBasicDisabledTextColor; - cursor: not-allowed; - } -} - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--primary { - color: $menuButtonPrimaryTextColor; - background-color: $menuButtonPrimaryBackgroundColor; - - // Goes before hover, so that hover can override it. - &:focus { - color: $menuButtonPrimaryTextColor !important; /* 1 */ - } - - &:hover, /* 2 */ - &:active { /* 2 */ - color: $menuButtonPrimaryTextColor !important; /* 1 */ - background-color: $menuButtonPrimaryHoverBackgroundColor; - } - - &:disabled { - background-color: $menuButtonPrimaryDisabledBackgroundColor; - cursor: not-allowed; - } -} - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--danger { - color: $menuButtonDangerTextColor; - background-color: $menuButtonDangerBackgroundColor; - - &:hover, /* 2 */ - &:active { /* 2 */ - color: $menuButtonDangerHoverTextColor !important; /* 1 */ - background-color: $menuButtonDangerHoverBackgroundColor; - } - - &:disabled { - color: $menuButtonDangerHoverDisabledTextColor; - background-color: $menuButtonDangerDisabledBackgroundColor; - cursor: not-allowed; - } - - &:focus { - @include focus($kuiFocusDangerColor); - } -} diff --git a/packages/kbn-ui-framework/src/components/menu_button/_menu_button_group.scss b/packages/kbn-ui-framework/src/components/menu_button/_menu_button_group.scss deleted file mode 100644 index e8a53967e8b3..000000000000 --- a/packages/kbn-ui-framework/src/components/menu_button/_menu_button_group.scss +++ /dev/null @@ -1,11 +0,0 @@ -.kuiMenuButtonGroup { - display: flex; - - .kuiMenuButton + .kuiMenuButton { - margin-left: 4px; - } -} - -.kuiMenuButtonGroup--alignRight { - justify-content: flex-end; -} diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/confirm_modal.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/confirm_modal.test.js.snap deleted file mode 100644 index b42842a6959c..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/confirm_modal.test.js.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiConfirmModal 1`] = ` -
    -
    -
    -
    - A confirmation modal -
    -
    -
    -
    -

    - This is a confirmation modal example -

    -
    -
    -
    - - -
    -
    -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal.test.js.snap deleted file mode 100644 index f2c725e3b38e..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal.test.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModal 1`] = ` -
    -
    - children -
    -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_body.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_body.test.js.snap deleted file mode 100644 index b0ddad312d8b..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_body.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalBody 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_footer.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_footer.test.js.snap deleted file mode 100644 index c4aaebfe7577..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_footer.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalFooter 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header.test.js.snap deleted file mode 100644 index 61fa239e7232..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalHeader 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header_title.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header_title.test.js.snap deleted file mode 100644 index 938e82898578..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header_title.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalHeaderTitle 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_overlay.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_overlay.test.js.snap deleted file mode 100644 index 536461942dd3..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_overlay.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalOverlay 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/_index.scss b/packages/kbn-ui-framework/src/components/modal/_index.scss deleted file mode 100644 index fc6f1747b507..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/_index.scss +++ /dev/null @@ -1,11 +0,0 @@ -$modalPadding: 10px; -$modalBorderColor: $euiBorderColor; -$modalBackgroundColor: $euiColorEmptyShade; -$kuiModalDepth: 1000; -$modalOverlayBackground: $euiColorEmptyShade; -@if (lightness($euiTextColor) > 50) { - $modalOverlayBackground: $euiColorLightShade; -} - -@import "modal_overlay"; -@import "modal"; diff --git a/packages/kbn-ui-framework/src/components/modal/_modal.scss b/packages/kbn-ui-framework/src/components/modal/_modal.scss deleted file mode 100644 index 0564db699837..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/_modal.scss +++ /dev/null @@ -1,63 +0,0 @@ -.kuiModal { - @include euiBottomShadow; - - line-height: $kuiLineHeight; - background-color: $modalBackgroundColor; - border: 1px solid $modalBorderColor; - border-radius: $kuiBorderRadius; - z-index: $kuiModalDepth + 1; - animation: kuiModal $kuiAnimSpeedSlow $kuiAnimSlightBounce; -} - -.kuiModal--confirmation { - width: 450px; - min-width: auto; -} - -.kuiModalHeader { - display: flex; - justify-content: space-between; - align-items: center; - padding: $modalPadding; - padding-left: $modalPadding * 2; - border-bottom: $kuiBorderThin; -} - - .kuiModalHeader__title { - font-size: $kuiTitleFontSize; - } - -.kuiModalHeaderCloseButton { - @include microButton; - font-size: $kuiTitleFontSize; -} - -.kuiModalBody { - padding: $modalPadding * 2; -} - -.kuiModalBodyText { - font-size: 14px; -} - -.kuiModalFooter { - display: flex; - justify-content: flex-end; - padding: $modalPadding * 2; - padding-top: $modalPadding; - - > * + * { - margin-left: 5px; - } -} - -@keyframes kuiModal { - 0% { - opacity: 0; - transform: translateY($kuiSizeXL); - } - 100% { - opacity: 1; - transform: translateY(0); - } -} diff --git a/packages/kbn-ui-framework/src/components/modal/_modal_overlay.scss b/packages/kbn-ui-framework/src/components/modal/_modal_overlay.scss deleted file mode 100644 index 6af8154e85c7..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/_modal_overlay.scss +++ /dev/null @@ -1,13 +0,0 @@ -.kuiModalOverlay { - position: fixed; - z-index: $kuiModalDepth; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - padding-bottom: 10vh; - background: transparentize($modalOverlayBackground, .2); -} diff --git a/packages/kbn-ui-framework/src/components/modal/confirm_modal.js b/packages/kbn-ui-framework/src/components/modal/confirm_modal.js deleted file mode 100644 index f6578231cec8..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/confirm_modal.js +++ /dev/null @@ -1,120 +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 PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import { KuiModal } from './modal'; -import { KuiModalFooter } from './modal_footer'; -import { KuiModalHeader } from './modal_header'; -import { KuiModalHeaderTitle } from './modal_header_title'; -import { KuiModalBody } from './modal_body'; -import { - KuiButton, -} from '../../components/'; - -export const CONFIRM_BUTTON = 'confirm'; -export const CANCEL_BUTTON = 'cancel'; - -const CONFIRM_MODAL_BUTTONS = [ - CONFIRM_BUTTON, - CANCEL_BUTTON, -]; - -export function KuiConfirmModal({ - children, - title, - onCancel, - onConfirm, - cancelButtonText, - confirmButtonText, - className, - defaultFocusedButton, - ...rest -}) { - const classes = classnames('kuiModal--confirmation', className); - - let modalTitle; - - if (title) { - modalTitle = ( - - - {title} - - - ); - } - - let message; - - if (typeof children === 'string') { - message =

    {children}

    ; - } else { - message = children; - } - - return ( - - {modalTitle} - - -
    - {message} -
    -
    - - - - {cancelButtonText} - - - - {confirmButtonText} - - -
    - ); -} - -KuiConfirmModal.propTypes = { - children: PropTypes.node, - title: PropTypes.string, - cancelButtonText: PropTypes.string, - confirmButtonText: PropTypes.string, - onCancel: PropTypes.func.isRequired, - onConfirm: PropTypes.func, - className: PropTypes.string, - defaultFocusedButton: PropTypes.oneOf(CONFIRM_MODAL_BUTTONS) -}; diff --git a/packages/kbn-ui-framework/src/components/modal/confirm_modal.test.js b/packages/kbn-ui-framework/src/components/modal/confirm_modal.test.js deleted file mode 100644 index c2713bebffd9..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/confirm_modal.test.js +++ /dev/null @@ -1,145 +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 sinon from 'sinon'; -import { mount, render } from 'enzyme'; - -import { findTestSubject, requiredProps } from '../../test'; -import { keyCodes } from '../../services'; - -import { - CANCEL_BUTTON, CONFIRM_BUTTON, KuiConfirmModal, -} from './confirm_modal'; - -let onConfirm; -let onCancel; - -beforeEach(() => { - onConfirm = sinon.spy(); - onCancel = sinon.spy(); -}); - -test('renders KuiConfirmModal', () => { - const component = render( - {}} - onConfirm={onConfirm} - cancelButtonText="Cancel Button Text" - confirmButtonText="Confirm Button Text" - {...requiredProps} - > - This is a confirmation modal example - - ); - expect(component).toMatchSnapshot(); -}); - -test('onConfirm', () => { - const component = mount( - - ); - - findTestSubject(component, 'confirmModalConfirmButton').simulate('click'); - sinon.assert.calledOnce(onConfirm); - sinon.assert.notCalled(onCancel); -}); - -describe('onCancel', () => { - test('triggered by click', () => { - const component = mount( - - ); - - findTestSubject(component, 'confirmModalCancelButton').simulate('click'); - sinon.assert.notCalled(onConfirm); - sinon.assert.calledOnce(onCancel); - }); - - test('triggered by esc key', () => { - const component = mount( - - ); - - findTestSubject(component, 'modal').simulate('keydown', { keyCode: keyCodes.ESCAPE }); - sinon.assert.notCalled(onConfirm); - sinon.assert.calledOnce(onCancel); - }); -}); - -describe('defaultFocusedButton', () => { - test('is cancel', () => { - const component = mount( - - ); - - const button = findTestSubject(component, 'confirmModalCancelButton').getDOMNode(); - expect(document.activeElement).toEqual(button); - }); - - test('is confirm', () => { - const component = mount( - - ); - - const button = findTestSubject(component, 'confirmModalConfirmButton').getDOMNode(); - expect(document.activeElement).toEqual(button); - }); - - test('when not given gives focus to the modal', () => { - const component = mount( - - ); - expect(document.activeElement).toEqual(component.getDOMNode().firstChild); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/index.js b/packages/kbn-ui-framework/src/components/modal/index.js deleted file mode 100644 index 7ce9e7e38606..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/index.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. - */ - -export { - KuiConfirmModal, - CONFIRM_BUTTON as KUI_MODAL_CONFIRM_BUTTON, - CANCEL_BUTTON as KUI_MODAL_CANCEL_BUTTON, -} from './confirm_modal'; -export { KuiModal } from './modal'; -export { KuiModalFooter } from './modal_footer'; -export { KuiModalHeader } from './modal_header'; -export { KuiModalOverlay } from './modal_overlay'; -export { KuiModalBody } from './modal_body'; -export { KuiModalHeaderTitle } from './modal_header_title'; diff --git a/packages/kbn-ui-framework/src/components/modal/modal.js b/packages/kbn-ui-framework/src/components/modal/modal.js deleted file mode 100644 index 0aa5d9ca5049..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal.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 React, { - Component, -} from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; -import FocusTrap from 'focus-trap-react'; - -import { keyCodes } from '../../services'; - -export class KuiModal extends Component { - onKeyDown = event => { - if (event.keyCode === keyCodes.ESCAPE) { - this.props.onClose(); - } - }; - - render() { - const { - className, - children, - onClose, // eslint-disable-line no-unused-vars - ...rest - } = this.props; - - const classes = classnames('kuiModal', className); - - return ( - this.modal, - }} - > - { - // Create a child div instead of applying these props directly to FocusTrap, or else - // fallbackFocus won't work. - } -
    { this.modal = node; }} - className={classes} - onKeyDown={this.onKeyDown} - tabIndex={0} - {...rest} - > - {children} -
    -
    - ); - } -} - -KuiModal.propTypes = { - className: PropTypes.string, - children: PropTypes.node, - onClose: PropTypes.func.isRequired, -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal.test.js b/packages/kbn-ui-framework/src/components/modal/modal.test.js deleted file mode 100644 index 0700079f5905..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal.test.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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModal, -} from './modal'; - -test('renders KuiModal', () => { - const component = ( - {}} - {...requiredProps} - > - children - - ); - - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_body.js b/packages/kbn-ui-framework/src/components/modal/modal_body.js deleted file mode 100644 index 36bef25ae9b1..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_body.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 React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalBody({ className, children, ...rest }) { - const classes = classnames('kuiModalBody', className); - return ( -
    - { children } -
    - ); -} - -KuiModalBody.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_body.test.js b/packages/kbn-ui-framework/src/components/modal/modal_body.test.js deleted file mode 100644 index b2a70839a12a..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_body.test.js +++ /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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalBody, -} from './modal_body'; - -test('renders KuiModalBody', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_footer.js b/packages/kbn-ui-framework/src/components/modal/modal_footer.js deleted file mode 100644 index 1ded35b7046e..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_footer.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 React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalFooter({ className, children, ...rest }) { - const classes = classnames('kuiModalFooter', className); - return ( -
    - { children } -
    - ); -} - -KuiModalFooter.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_footer.test.js b/packages/kbn-ui-framework/src/components/modal/modal_footer.test.js deleted file mode 100644 index e8404292ce1e..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_footer.test.js +++ /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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalFooter, -} from './modal_footer'; - -test('renders KuiModalFooter', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_header.js b/packages/kbn-ui-framework/src/components/modal/modal_header.js deleted file mode 100644 index 9165e0c0a5b1..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_header.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 React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalHeader({ className, children, ...rest }) { - const classes = classnames('kuiModalHeader', className); - return ( -
    - { children } -
    - ); -} - -KuiModalHeader.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_header.test.js b/packages/kbn-ui-framework/src/components/modal/modal_header.test.js deleted file mode 100644 index 10a51a1da4a8..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_header.test.js +++ /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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalHeader, -} from './modal_header'; - -test('renders KuiModalHeader', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_header_title.js b/packages/kbn-ui-framework/src/components/modal/modal_header_title.js deleted file mode 100644 index 47d1960e02ae..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_header_title.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 React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalHeaderTitle({ className, children, ...rest }) { - const classes = classnames('kuiModalHeader__title', className); - return ( -
    - { children } -
    - ); -} - -KuiModalHeaderTitle.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_header_title.test.js b/packages/kbn-ui-framework/src/components/modal/modal_header_title.test.js deleted file mode 100644 index 5ca29edfa972..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_header_title.test.js +++ /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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalHeaderTitle, -} from './modal_header_title'; - -test('renders KuiModalHeaderTitle', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_overlay.js b/packages/kbn-ui-framework/src/components/modal/modal_overlay.js deleted file mode 100644 index 4d9ed64b8ead..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_overlay.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 React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalOverlay({ className, ...rest }) { - const classes = classnames('kuiModalOverlay', className); - return ( -
    - ); -} - -KuiModalOverlay.propTypes = { - className: PropTypes.string, -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_overlay.test.js b/packages/kbn-ui-framework/src/components/modal/modal_overlay.test.js deleted file mode 100644 index 75c3feee14e3..000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_overlay.test.js +++ /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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalOverlay, -} from './modal_overlay'; - -test('renders KuiModalOverlay', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap b/packages/kbn-ui-framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap deleted file mode 100644 index 6345a66dd3ab..000000000000 --- a/packages/kbn-ui-framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiOutsideClickDetector is rendered 1`] = `
    `; diff --git a/packages/kbn-ui-framework/src/components/outside_click_detector/index.js b/packages/kbn-ui-framework/src/components/outside_click_detector/index.js deleted file mode 100644 index 52e22b67af17..000000000000 --- a/packages/kbn-ui-framework/src/components/outside_click_detector/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 { - KuiOutsideClickDetector, -} from './outside_click_detector'; diff --git a/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.js b/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.js deleted file mode 100644 index 0d107c249467..000000000000 --- a/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.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 { - Children, - cloneElement, - Component, -} from 'react'; -import PropTypes from 'prop-types'; - -export class KuiOutsideClickDetector extends Component { - static propTypes = { - children: PropTypes.node.isRequired, - onOutsideClick: PropTypes.func.isRequired, - }; - - onClickOutside = event => { - if (!this.wrapperRef) { - return; - } - - if (this.wrapperRef === event.target) { - return; - } - - if (this.wrapperRef.contains(event.target)) { - return; - } - - this.props.onOutsideClick(); - }; - - componentDidMount() { - document.addEventListener('mousedown', this.onClickOutside); - } - - componentWillUnmount() { - document.removeEventListener('mousedown', this.onClickOutside); - } - - render() { - const props = { - ...this.props.children.props, - ref: node => { - this.wrapperRef = node; - }, - }; - - const child = Children.only(this.props.children); - return cloneElement(child, props); - } -} diff --git a/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.test.js b/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.test.js deleted file mode 100644 index 5d872531f07b..000000000000 --- a/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.test.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 React from 'react'; -import { render } from 'enzyme'; - -import { KuiOutsideClickDetector } from './outside_click_detector'; - -describe('KuiOutsideClickDetector', () => { - test('is rendered', () => { - const component = render( - {}}> -
    - - ); - - expect(component) - .toMatchSnapshot(); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/panel_simple/__snapshots__/panel_simple.test.js.snap b/packages/kbn-ui-framework/src/components/panel_simple/__snapshots__/panel_simple.test.js.snap deleted file mode 100644 index f826cf618273..000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/__snapshots__/panel_simple.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiPanelSimple is rendered 1`] = ` -
    -`; diff --git a/packages/kbn-ui-framework/src/components/panel_simple/_index.scss b/packages/kbn-ui-framework/src/components/panel_simple/_index.scss deleted file mode 100644 index 31e793246dfe..000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'panel_simple'; diff --git a/packages/kbn-ui-framework/src/components/panel_simple/_panel_simple.scss b/packages/kbn-ui-framework/src/components/panel_simple/_panel_simple.scss deleted file mode 100644 index debc03f0cfac..000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/_panel_simple.scss +++ /dev/null @@ -1,27 +0,0 @@ -.kuiPanelSimple { - @include euiBottomShadowSmall; - background-color: $euiColorEmptyShade; - border: $euiBorderThin; - border-radius: $euiBorderRadius; - flex-grow: 1; - - &.kuiPanelSimple--paddingSmall { - padding: $kuiSizeS; - } - - &.kuiPanelSimple--paddingMedium { - padding: $kuiSize; - } - - &.kuiPanelSimple--paddingLarge { - padding: $kuiSizeL; - } - - &.kuiPanelSimple--shadow { - @include kuiBottomShadow; - } - - &.kuiPanelSimple--flexGrowZero { - flex-grow: 0; - } -} diff --git a/packages/kbn-ui-framework/src/components/panel_simple/index.js b/packages/kbn-ui-framework/src/components/panel_simple/index.js deleted file mode 100644 index c9375a948883..000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/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 { - KuiPanelSimple, - SIZES, -} from './panel_simple'; diff --git a/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.js b/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.js deleted file mode 100644 index 8e902201ced0..000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.js +++ /dev/null @@ -1,78 +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 PropTypes from 'prop-types'; -import classNames from 'classnames'; - -const paddingSizeToClassNameMap = { - 'none': null, - 's': 'kuiPanelSimple--paddingSmall', - 'm': 'kuiPanelSimple--paddingMedium', - 'l': 'kuiPanelSimple--paddingLarge', -}; - -export const SIZES = Object.keys(paddingSizeToClassNameMap); - -export const KuiPanelSimple = ({ - children, - className, - paddingSize, - hasShadow, - grow, - panelRef, - ...rest -}) => { - - const classes = classNames( - 'kuiPanelSimple', - paddingSizeToClassNameMap[paddingSize], - { - 'kuiPanelSimple--shadow': hasShadow, - 'kuiPanelSimple--flexGrowZero': !grow, - }, - className - ); - - return ( -
    - {children} -
    - ); - -}; - -KuiPanelSimple.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - hasShadow: PropTypes.bool, - paddingSize: PropTypes.oneOf(SIZES), - grow: PropTypes.bool, - panelRef: PropTypes.func, -}; - -KuiPanelSimple.defaultProps = { - paddingSize: 'm', - hasShadow: false, - grow: true, -}; diff --git a/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.test.js b/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.test.js deleted file mode 100644 index 977011207a16..000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.test.js +++ /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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { KuiPanelSimple } from './panel_simple'; - -describe('KuiPanelSimple', () => { - test('is rendered', () => { - const component = render( - - ); - - expect(component) - .toMatchSnapshot(); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover.test.js.snap b/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover.test.js.snap deleted file mode 100644 index a409fcd28b80..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover.test.js.snap +++ /dev/null @@ -1,125 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiPopover children is rendered 1`] = ` -
    -
    -`; - -exports[`KuiPopover is rendered 1`] = ` -
    -
    -`; - -exports[`KuiPopover props anchorPosition defaults to center 1`] = ` -
    -
    -`; - -exports[`KuiPopover props anchorPosition left is rendered 1`] = ` -
    -
    -`; - -exports[`KuiPopover props anchorPosition right is rendered 1`] = ` -
    -
    -`; - -exports[`KuiPopover props isOpen defaults to false 1`] = ` -
    -
    -`; - -exports[`KuiPopover props isOpen renders true 1`] = ` -
    -
    -`; diff --git a/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover_title.test.js.snap b/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover_title.test.js.snap deleted file mode 100644 index adb2c559e5f9..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover_title.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiPopoverTitle is rendered 1`] = ` -
    -`; diff --git a/packages/kbn-ui-framework/src/components/popover/_index.scss b/packages/kbn-ui-framework/src/components/popover/_index.scss deleted file mode 100644 index e6b242e72507..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'mixins'; -@import 'popover'; -@import 'popover_title'; diff --git a/packages/kbn-ui-framework/src/components/popover/_mixins.scss b/packages/kbn-ui-framework/src/components/popover/_mixins.scss deleted file mode 100644 index c1abba14151b..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/_mixins.scss +++ /dev/null @@ -1,6 +0,0 @@ -@mixin kuiPopoverTitle { - background-color: $kuiColorLightestShade; - border-bottom: $kuiBorderThin; - padding: $kuiSizeM; - font-size: $kuiFontSize; -} diff --git a/packages/kbn-ui-framework/src/components/popover/_popover.scss b/packages/kbn-ui-framework/src/components/popover/_popover.scss deleted file mode 100644 index 2783fbe91027..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/_popover.scss +++ /dev/null @@ -1,96 +0,0 @@ -// Pop menu is an animated popover relatively positioned to a button / action. -// By default it positions in the middle, but can be anchored left and right. - -.kuiPopover { - display: inline-block; - position: relative; - - // Open state happens on the wrapper and applies to the panel. - &.kuiPopover-isOpen { - .kuiPopover__panel { - opacity: 1; - visibility: visible; - margin-top: $kuiSizeS; - pointer-events: auto; - } - } -} - - // Animation happens on the panel. - .kuiPopover__panel { - position: absolute; - z-index: $kuiZContentMenu; - top: 100%; - left: 50%; - transform: translateX(-50%) translateY($kuiSizeS) translateZ(0); - backface-visibility: hidden; - transition: - opacity $kuiAnimSlightBounce $kuiAnimSpeedSlow, - visibility $kuiAnimSlightBounce $kuiAnimSpeedSlow, - margin-top $kuiAnimSlightBounce $kuiAnimSpeedSlow; - transform-origin: center top; - opacity: 0; - visibility: hidden; - pointer-events: none; - margin-top: $kuiSizeL; - - // This fakes a border on the arrow. - &:before { - position: absolute; - content: ""; - top: -$kuiSize; - height: 0; - width: 0; - left: 50%; - margin-left: -$kuiSize; - border-left: $kuiSize solid transparent; - border-right: $kuiSize solid transparent; - border-bottom: $kuiSize solid $kuiBorderColor; - } - - // This part of the arrow matches the panel. - &:after { - position: absolute; - content: ""; - top: -$kuiSize + 1; - right: 0; - height: 0; - left: 50%; - margin-left: -$kuiSize; - width: 0; - border-left: $kuiSize solid transparent; - border-right: $kuiSize solid transparent; - border-bottom: $kuiSize solid $euiColorEmptyShade; - } - } - -.kuiPopover--withTitle .kuiPopover__panel:after { - border-bottom-color: $kuiColorLightestShade; -} - -// Positions the menu and arrow to the left of the parent. -.kuiPopover--anchorLeft { - .kuiPopover__panel { - left: 0; - transform: translateX(0%) translateY($kuiSizeS) translateZ(0); - - &:before, &:after { - right: auto; - left: $kuiSize; - margin: 0; - } - } -} - -// Positions the menu and arrow to the right of the parent. -.kuiPopover--anchorRight { - .kuiPopover__panel { - left: 100%; - transform: translateX(-100%) translateY($kuiSizeS) translateZ(0); - - &:before, &:after { - right: $kuiSize; - left: auto; - } - } -} diff --git a/packages/kbn-ui-framework/src/components/popover/_popover_title.scss b/packages/kbn-ui-framework/src/components/popover/_popover_title.scss deleted file mode 100644 index a5a60f1dfbff..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/_popover_title.scss +++ /dev/null @@ -1,3 +0,0 @@ -.kuiPopoverTitle { - @include kuiPopoverTitle; -} diff --git a/packages/kbn-ui-framework/src/components/popover/index.js b/packages/kbn-ui-framework/src/components/popover/index.js deleted file mode 100644 index 397993ed3b9f..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/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 { KuiPopover, } from './popover'; -export { KuiPopoverTitle } from './popover_title'; diff --git a/packages/kbn-ui-framework/src/components/popover/popover.js b/packages/kbn-ui-framework/src/components/popover/popover.js deleted file mode 100644 index 46360ee869d4..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/popover.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 React, { - Component, -} from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import FocusTrap from 'focus-trap-react'; -import tabbable from 'tabbable'; - -import { cascadingMenuKeyCodes } from '../../services'; - -import { KuiOutsideClickDetector } from '../outside_click_detector'; - -import { KuiPanelSimple, SIZES } from '../../components/panel_simple'; - -const anchorPositionToClassNameMap = { - 'center': '', - 'left': 'kuiPopover--anchorLeft', - 'right': 'kuiPopover--anchorRight', -}; - -export const ANCHOR_POSITIONS = Object.keys(anchorPositionToClassNameMap); - -export class KuiPopover extends Component { - constructor(props) { - super(props); - - this.closingTransitionTimeout = undefined; - - this.state = { - isClosing: false, - isOpening: false, - }; - } - - onKeyDown = e => { - if (e.keyCode === cascadingMenuKeyCodes.ESCAPE) { - this.props.closePopover(); - } - }; - - updateFocus() { - // Wait for the DOM to update. - window.requestAnimationFrame(() => { - if (!this.panel) { - return; - } - - // If we've already focused on something inside the panel, everything's fine. - if (this.panel.contains(document.activeElement)) { - return; - } - - // Otherwise let's focus the first tabbable item and expedite input from the user. - const tabbableItems = tabbable(this.panel); - if (tabbableItems.length) { - tabbableItems[0].focus(); - } - }); - } - - componentDidMount() { - this.updateFocus(); - } - - componentWillReceiveProps(nextProps) { - // The popover is being opened. - if (!this.props.isOpen && nextProps.isOpen) { - clearTimeout(this.closingTransitionTimeout); - // We need to set this state a beat after the render takes place, so that the CSS - // transition can take effect. - window.requestAnimationFrame(() => { - this.setState({ - isOpening: true, - }); - }); - } - - // The popover is being closed. - if (this.props.isOpen && !nextProps.isOpen) { - // If the user has just closed the popover, queue up the removal of the content after the - // transition is complete. - this.setState({ - isClosing: true, - isOpening: false, - }); - - this.closingTransitionTimeout = setTimeout(() => { - this.setState({ - isClosing: false, - }); - }, 250); - } - } - - componentDidUpdate() { - this.updateFocus(); - } - - componentWillUnmount() { - clearTimeout(this.closingTransitionTimeout); - } - - panelRef = node => { - if (this.props.ownFocus) { - this.panel = node; - } - }; - - render() { - const { - anchorPosition, - button, - isOpen, - ownFocus, - withTitle, - children, - className, - closePopover, - panelClassName, - panelPaddingSize, - ...rest - } = this.props; - - const classes = classNames( - 'kuiPopover', - anchorPositionToClassNameMap[anchorPosition], - className, - { - 'kuiPopover-isOpen': this.state.isOpening, - 'kuiPopover--withTitle': withTitle, - }, - ); - - const panelClasses = classNames('kuiPopover__panel', panelClassName); - - let panel; - - if (isOpen || this.state.isClosing) { - let tabIndex; - let initialFocus; - - if (ownFocus) { - tabIndex = '0'; - initialFocus = () => this.panel; - } - - panel = ( - - - {children} - - - ); - } - - return ( - -
    - {button} - {panel} -
    -
    - ); - } -} - -KuiPopover.propTypes = { - isOpen: PropTypes.bool, - ownFocus: PropTypes.bool, - withTitle: PropTypes.bool, - closePopover: PropTypes.func.isRequired, - button: PropTypes.node.isRequired, - children: PropTypes.node, - anchorPosition: PropTypes.oneOf(ANCHOR_POSITIONS), - panelClassName: PropTypes.string, - panelPaddingSize: PropTypes.oneOf(SIZES), -}; - -KuiPopover.defaultProps = { - isOpen: false, - ownFocus: false, - anchorPosition: 'center', - panelPaddingSize: 'm', -}; diff --git a/packages/kbn-ui-framework/src/components/popover/popover.test.js b/packages/kbn-ui-framework/src/components/popover/popover.test.js deleted file mode 100644 index 683fdaf0525c..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/popover.test.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 React from 'react'; -import { render, mount } from 'enzyme'; -import sinon from 'sinon'; -import { requiredProps } from '../../test/required_props'; - -import { KuiPopover } from './popover'; - -import { keyCodes } from '../../services'; - -describe('KuiPopover', () => { - test('is rendered', () => { - const component = render( - } - closePopover={() => {}} - {...requiredProps} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('children is rendered', () => { - const component = render( - } - closePopover={() => {}} - > - Children - - ); - - expect(component) - .toMatchSnapshot(); - }); - - describe('props', () => { - describe('withTitle', () => { - test('is rendered', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('closePopover', () => { - it('is called when ESC key is hit', () => { - const closePopoverHandler = sinon.stub(); - - const component = mount( - } - closePopover={closePopoverHandler} - /> - ); - - component.simulate('keydown', { keyCode: keyCodes.ESCAPE }); - sinon.assert.calledOnce(closePopoverHandler); - }); - }); - - describe('anchorPosition', () => { - test('defaults to center', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('left is rendered', () => { - const component = render( - } - closePopover={() => {}} - anchorPosition="left" - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('right is rendered', () => { - const component = render( - } - closePopover={() => {}} - anchorPosition="right" - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('isOpen', () => { - test('defaults to false', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('renders true', () => { - const component = render( - } - closePopover={() => {}} - isOpen - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('ownFocus', () => { - test('defaults to false', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('renders true', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('panelClassName', () => { - test('is rendered', () => { - const component = render( - } - closePopover={() => {}} - panelClassName="test" - isOpen - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('panelPaddingSize', () => { - test('is rendered', () => { - const component = render( - } - closePopover={() => {}} - panelPaddingSize="s" - isOpen - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/popover/popover_title.js b/packages/kbn-ui-framework/src/components/popover/popover_title.js deleted file mode 100644 index 88aae95d400c..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/popover_title.js +++ /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 React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export const KuiPopoverTitle = ({ children, className, ...rest }) => { - const classes = classNames('kuiPopoverTitle', className); - - return ( -
    - {children} -
    - ); -}; - -KuiPopoverTitle.propTypes = { - children: PropTypes.node, - className: PropTypes.string, -}; diff --git a/packages/kbn-ui-framework/src/components/popover/popover_title.test.js b/packages/kbn-ui-framework/src/components/popover/popover_title.test.js deleted file mode 100644 index 308afcfa97ca..000000000000 --- a/packages/kbn-ui-framework/src/components/popover/popover_title.test.js +++ /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 React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { KuiPopoverTitle } from './popover_title'; - -describe('KuiPopoverTitle', () => { - test('is rendered', () => { - const component = render( - - ); - - expect(component) - .toMatchSnapshot(); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/toggle_button/_index.scss b/packages/kbn-ui-framework/src/components/toggle_button/_index.scss deleted file mode 100644 index 80ad91ff370f..000000000000 --- a/packages/kbn-ui-framework/src/components/toggle_button/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "toggle_button"; -@import "toggle_panel"; diff --git a/packages/kbn-ui-framework/src/components/toggle_button/_toggle_button.scss b/packages/kbn-ui-framework/src/components/toggle_button/_toggle_button.scss deleted file mode 100644 index 81e4198f7978..000000000000 --- a/packages/kbn-ui-framework/src/components/toggle_button/_toggle_button.scss +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 1. Allow container to determine font-size and line-height. - * 2. Override inherited Bootstrap styles. - */ -.kuiToggleButton { - appearance: none; - cursor: pointer; - background-color: transparent; - border: none; - padding: 0; - font-size: inherit; /* 1 */ - line-height: inherit; /* 1 */ - color: $kuiFontColor; - - &:focus { - color: $kuiFontColor; - } - - &:active { - color: $kuiLinkColor !important; /* 2 */ - } - - &:hover:not(:disabled) { - color: $kuiLinkHoverColor !important; /* 2 */ - text-decoration: underline; - } - - &:disabled { - cursor: not-allowed; - opacity: .5; - } -} - - /** - * 1. Make icon a consistent width so the text doesn't get pushed around as the icon changes - * between "expand" and "collapse". Use ems to be relative to inherited font-size. - */ - .kuiToggleButton__icon { - width: 0.8em; /* 1 */ - } diff --git a/packages/kbn-ui-framework/src/components/toggle_button/_toggle_panel.scss b/packages/kbn-ui-framework/src/components/toggle_button/_toggle_panel.scss deleted file mode 100644 index afa779b37481..000000000000 --- a/packages/kbn-ui-framework/src/components/toggle_button/_toggle_panel.scss +++ /dev/null @@ -1,13 +0,0 @@ -.kuiTogglePanelHeader { - padding-bottom: 4px; - margin-bottom: 15px; - border-bottom: $kuiBorderThin; - - /** - * 1. Allow the user to click anywhere on the header, not just on the button text. - */ - .kuiToggleButton { - width: 100%; /* 1 */ - text-align: left; /* 1 */ - } -} diff --git a/renovate.json5 b/renovate.json5 index ef38292d024e..3e9c17c7fe0a 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -616,6 +616,14 @@ '@types/history', ], }, + { + groupSlug: 'jsdom', + groupName: 'jsdom related packages', + packageNames: [ + 'jsdom', + '@types/jsdom', + ], + }, { groupSlug: 'jsonwebtoken', groupName: 'jsonwebtoken related packages', diff --git a/rfcs/text/0005_route_handler.md b/rfcs/text/0005_route_handler.md new file mode 100644 index 000000000000..291da688bc5a --- /dev/null +++ b/rfcs/text/0005_route_handler.md @@ -0,0 +1,185 @@ +- Start Date: 2019-06-29 +- RFC PR: (leave this empty) +- Kibana Issue: https://github.com/elastic/kibana/issues/33779 + +# Summary + +Http Service in New platform should provide the ability to execute some logic in response to an incoming request and send the result of this operation back. + +# Basic example +Declaring a route handler for `/url` endpoint: +```typescript +router.get( + { path: '/url', ...[otherRouteParameters] }, + (context: Context, request: KibanaRequest, t: KibanaResponseToolkit) => { + // logic to handle request ... + return t.ok(result); +); + +``` + +# Motivation +The new platform is built with library-agnostic philosophy and we cannot transfer the current solution for Network layer from Hapi. To avoid vendor lock-in in the future, we have to define route handler logic and request/response objects formats that can be implemented in any low-level library such as Express, Hapi, etc. It means that we are going to operate our own abstractions for such Http domain entities as Router, Route, Route Handler, Request, Response. + +# Detailed design +The new platform doesn't support the Legacy platform `Route Handler` format nor exposes implementation details, such as [Hapi.ResponseToolkit](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/hapi/v17/index.d.ts#L984). +Rather `Route Handler` in New platform has the next signature: +```typescript +type RequestHandler = ( + context: Context, + request: KibanaRequest, + t: KibanaResponseToolkit +) => KibanaResponse | Promise; +``` +and accepts next Kibana specific parameters as arguments: +- context: [Context](https://github.com/elastic/kibana/blob/master/rfcs/text/0003_handler_interface.md#handler-context). A handler context contains core service and plugin functionality already scoped to the incoming request. +- request: [KibanaRequest](https://github.com/elastic/kibana/blob/master/src/core/server/http/router/request.ts). An immutable representation of the incoming request details, such as body, parameters, query, url and route information. Note: you **must** to specify route schema during route declaration to have access to `body, parameters, query` in the request object. You cannot extend KibanaRequest with arbitrary data nor remove any properties from it. +```typescript +interface KibanaRequest { + url: url.Url; + headers: Record; + params?: Record; + body?: Record; + query?: Record; + route: { + path: string; + method: 'get' | 'post' | ... + options: { + authRequired: boolean; + tags: string []; + } + } +} +``` +- t: [KibanaResponseToolkit](https://github.com/elastic/kibana/blob/master/src/core/server/http/router/response.ts#L27) +Provides a set of pre-configured methods to respond to an incoming request. It is expected that handler **always** returns a result of one of `KibanaResponseToolkit` methods as an output: +```typescript +interface KibanaResponseToolkit { + [method:string]: (...params: any) => KibanaResponse +} +router.get(..., + (context: Context, request: KibanaRequest, t: KibanaResponseToolkit): KibanaResponse => { + return t.ok(); + // or + return t.redirected('/url'); + // or + return t.badRequest(error); +); +``` +*KibanaResponseToolkit* methods allow an end user to adjust the next response parameters: +- Body. Supported values:`undefined | string | JSONValue | Buffer | Stream`. +- Status code. +- Headers. Supports adjusting [known values](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v10/http.d.ts#L8) and attaching [custom values as well](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v10/http.d.ts#L67) + +Other response parameters, such as `etag`, `MIME-type`, `bytes` that used in the Legacy platform could be adjusted via Headers. + +The router handler doesn't expect that logic inside can throw or return something different from `KibanaResponse`. In this case, Http service will respond with `Server error` to prevent exposure of internal logic details. + +#### KibanaResponseToolkit methods +Basic primitives: +```typescript +type HttpResponsePayload = undefined | string | JSONValue | Buffer | Stream; +interface HttpResponseOptions { + headers?: { + // list of known headers + ... + // for custom headers: + [header: string]: string | string[]; + } +} + +``` + +##### Success +Server indicated that request was accepted: +```typescript +type SuccessResponse = ( + payload: T, + options?: HttpResponseOptions +) => KibanaResponse; + +const kibanaResponseToolkit = { + ok: (payload: T, options?: HttpResponseOptions) => + new KibanaResponse(200, payload, options), + accepted: (payload: T, options?: HttpResponseOptions) => + new KibanaResponse(202, payload, options), + noContent: (options?: HttpResponseOptions) => new KibanaResponse(204, undefined, options) +``` + +##### Redirection +The server wants a user to perform additional actions. +```typescript +const kibanaResponseToolkit = { + redirected: (url: string, options?: HttpResponseOptions) => new KibanaResponse(302, url, options), + notModified: (options?: HttpResponseOptions) => new KibanaResponse(304, undefined, options), +``` + +##### Error +Server signals that request cannot be handled and explains details of the error situation +```typescript +// Supports attaching additional data to send to the client +interface ResponseError extends Error { + meta?: { + data?: JSONValue; + errorCode?: string; // error code to simplify search, translations in i18n, etc. + docLink?: string; // link to the docs + } +} + +export const createResponseError = (error: Error | string, meta?: ResponseErrorType['meta']) => + new ResponseError(error, meta) + +const kibanaResponseToolkit = { + // Client errors + badRequest: (err: T, options?: HttpResponseOptions) => + new KibanaResponse(400, err, options), + unauthorized: (err: T, options?: HttpResponseOptions) => + new KibanaResponse(401, err, options), + + forbidden: (err: T, options?: HttpResponseOptions) => + new KibanaResponse(403, err, options), + notFound: (err: T, options?: HttpResponseOptions) => + new KibanaResponse(404, err, options), + conflict: (err: T, options?: HttpResponseOptions) => + new KibanaResponse(409, err, options), + + // Server errors + internal: (err: T, options?: HttpResponseOptions) => + new KibanaResponse(500, err, options), +``` + +##### Custom +If a custom response is required +```typescript +interface CustomOptions extends HttpResponseOptions { + statusCode: number; +} +export const kibanaResponseToolkit = { + custom: (payload: T, {statusCode, ...options}: CustomOptions) => + new KibanaResponse(statusCode, payload, options), +``` +# Drawbacks +- `Handler` is not compatible with Legacy platform implementation when anything can be returned or thrown from handler function and server send it as a valid result. Transition to the new format may require additional work in plugins. +- `Handler` doesn't cover **all** functionality of the Legacy server at the current moment. For example, we cannot render a view in New platform yet and in this case, we have to proxy the request to the Legacy platform endpoint to perform rendering. All such cases should be considered in an individual order. +- `KibanaResponseToolkit` may not cover all use cases and requires an extension for specific use-cases. +- `KibanaResponseToolkit` operates low-level Http primitives, such as Headers e.g., and it is not always handy to work with them directly. +- `KibanaResponse` cannot be extended with arbitrary data. + +# Alternatives + +- `Route Handler` may adopt well-known Hapi-compatible format. +- `KibanaResponseToolkit` can expose only one method that allows specifying any type of response body, headers, status without creating additional abstractions and restrictions. +- `KibanaResponseToolkit` may provide helpers for more granular use-cases, say ` +binary(data: Buffer, type: MimeType, size: number) => KibanaResponse` + +# Adoption strategy + +Breaking changes are expected during migration to the New platform. To simplify adoption we could provide an extended set of type definitions for primitives with high variability of possible values (such as content-type header, all headers in general). + +# How we teach this + +`Route Handler`, `Request`, `Response` terms are familiar to all Kibana developers. Even if their interface is different from existing ones, it shouldn't be a problem to adopt the code to the new format. Adding a section to the Migration guide should be sufficient. + +# Unresolved questions + +Is proposed functionality cover all the use cases of the `Route Handler` and responding to a request? diff --git a/scripts/es.js b/scripts/es.js index 26e8ed5f7f69..93f1d69350ba 100644 --- a/scripts/es.js +++ b/scripts/es.js @@ -17,12 +17,12 @@ * under the License. */ +require('../src/setup_node_env'); + var resolve = require('path').resolve; var pkg = require('../package.json'); var kbnEs = require('@kbn/es'); -require('../src/setup_node_env'); - kbnEs .run({ license: 'basic', @@ -30,6 +30,7 @@ kbnEs version: pkg.version, 'source-path': resolve(__dirname, '../../elasticsearch'), 'base-path': resolve(__dirname, '../.es'), + ssl: false, }) .catch(function (e) { console.error(e); diff --git a/scripts/functional_test_runner.js b/scripts/functional_test_runner.js index 6b298e9b3031..e0ed92c958dd 100644 --- a/scripts/functional_test_runner.js +++ b/scripts/functional_test_runner.js @@ -18,4 +18,4 @@ */ require('../src/setup_node_env'); -require('../src/functional_test_runner/cli'); +require('@kbn/test').runFtrCli(); diff --git a/scripts/functional_tests_server.js b/scripts/functional_tests_server.js index 156da2dfbbe1..53f6d0f67f25 100644 --- a/scripts/functional_tests_server.js +++ b/scripts/functional_tests_server.js @@ -18,6 +18,6 @@ */ require('../src/setup_node_env'); -require('../packages/kbn-test').startServersCli( +require('@kbn/test').startServersCli( require.resolve('../test/functional/config.js'), ); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 5bb50b55269d..7f479a7e118e 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -20,6 +20,7 @@ import _ from 'lodash'; import { statSync } from 'fs'; import { resolve } from 'path'; +import url from 'url'; import { fromRoot, IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; import { getConfig } from '../../legacy/server/path'; @@ -87,12 +88,37 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { } if (opts.ssl) { - set('server.ssl.enabled', true); - } + // @kbn/dev-utils is part of devDependencies + const { CA_CERT_PATH } = require('@kbn/dev-utils'); + const customElasticsearchHosts = opts.elasticsearch + ? opts.elasticsearch.split(',') + : [].concat(get('elasticsearch.hosts') || []); + + function ensureNotDefined(path) { + if (has(path)) { + throw new Error(`Can't use --ssl when "${path}" configuration is already defined.`); + } + } + ensureNotDefined('server.ssl.certificate'); + ensureNotDefined('server.ssl.key'); + ensureNotDefined('elasticsearch.ssl.certificateAuthorities'); + + const elasticsearchHosts = ( + (customElasticsearchHosts.length > 0 && customElasticsearchHosts) || + ['https://localhost:9200'] + ).map(hostUrl => { + const parsedUrl = url.parse(hostUrl); + if (parsedUrl.hostname !== 'localhost') { + throw new Error(`Hostname "${parsedUrl.hostname}" can't be used with --ssl. Must be "localhost" to work with certificates.`); + } + return `https://localhost:${parsedUrl.port}`; + }); - if (opts.ssl && !has('server.ssl.certificate') && !has('server.ssl.key')) { + set('server.ssl.enabled', true); set('server.ssl.certificate', DEV_SSL_CERT_PATH); set('server.ssl.key', DEV_SSL_KEY_PATH); + set('elasticsearch.hosts', elasticsearchHosts); + set('elasticsearch.ssl.certificateAuthorities', CA_CERT_PATH); } } diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index a3b6b715b131..af4aef9f907e 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -27,7 +27,7 @@ * [How do I build my shim for New Platform services?](#how-do-i-build-my-shim-for-new-platform-services) * [How to](#how-to) * [Configure plugin](#configure-plugin) - * [Mock core services in tests](#mock-core-services-in-tests) + * [Mock new platform services in tests](#mock-new-platform-services-in-tests) Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. @@ -604,9 +604,155 @@ It is generally a much greater challenge preparing legacy browser-side code for To complicate matters further, a significant amount of the business logic in Kibana's client-side code exists inside the `ui/public` directory (aka ui modules), and all of that must be migrated as well. Unlike the server-side code where the order in which you migrated plugins was not particularly important, it's important that UI modules be addressed as soon as possible. -Also unlike the server-side migration, we won't concern ourselves with creating shimmed plugin definitions that then get copied over to complete the migration. +Because usage of angular and `ui/public` modules varies widely between legacy plugins, there is no "one size fits all" solution to migrating your browser-side code to the new platform. The best place to start is by checking with the platform team to help identify the best migration path for your particular plugin. -### Move UI modules into plugins +That said, we've seen a series of patterns emerge as teams begin migrating browser code. In practice, most migrations will follow a path that looks something like this: + +#### 1. Create a plugin definition file + +We've found that doing this right away helps you start thinking about your plugin in terms of lifecycle methods and services, which makes the rest of the migration process feel more natural. It also forces you to identify which actions "kick off" your plugin, since you'll need to execute those when the `setup/start` methods are called. + +This definition isn't going to do much for us just yet, but as we get further into the process, we will gradually start returning contracts from our `setup` and `start` methods, while also injecting dependencies as arguments to these methods. + +```ts +// public/plugin.ts +import { CoreSetup, CoreStart, Plugin } from '../../../../core/public'; +import { FooSetup, FooStart } from '../../../../legacy/core_plugins/foo/public'; + +/** + * These are the private interfaces for the services your plugin depends on. + * @internal + */ +export interface DemoSetupDeps { + foo: FooSetup; +} +export interface DemoStartDeps { + foo: FooStart; +} + +/** + * These are the interfaces with your public contracts. You should export these + * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. + * @public + */ +export type DemoSetup = {} +export type DemoStart = {} + +/** @internal */ +export class DemoPlugin implements Plugin { + public setup(core: CoreSetup, plugins: DemoSetupDeps): DemoSetup { + // kick off your plugin here... + return { + fetchConfig: () => ({}), + }; + } + + public start(core: CoreStart, plugins: DemoStartDeps): DemoStart { + // ...or here + return { + initDemo: () => ({}), + }; + } + + public stop() {} +} +``` + +#### 2. Export all static code and types from `public/index.ts` + +If your plugin needs to share static code with other plugins, this code must be exported from your top-level `public/index.ts`. This includes any type interfaces that you wish to make public. For details on the types of code that you can safely share outside of the runtime lifecycle contracts, see [Can static code be shared between plugins?](#can-static-code-be-shared-between-plugins) + +```ts +// public/index.ts +import { DemoSetup, DemoStart } from './plugin'; + +const myPureFn = (x: number): number => x + 1; +const MyReactComponent = (props) => { + return

    Hello, {props.name}

    ; +} + +// These are your public types & static code +export { + myPureFn, + MyReactComponent, + DemoSetup, + DemoStart, +} +``` + +While you're at it, you can also add your plugin initializer to this file: + +```ts +// public/index.ts +import { PluginInitializer, PluginInitializerContext } from '../../../../core/public'; +import { DemoSetup, DemoStart, DemoSetupDeps, DemoStartDeps, DemoPlugin } from './plugin'; + +// Core will be looking for this when loading our plugin in the new platform +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => { + return new DemoPlugin(); +}; + +const myPureFn = (x: number): number => x + 1; +const MyReactComponent = (props) => { + return

    Hello, {props.name}

    ; +} + +/** @public */ +export { + myPureFn, + MyReactComponent, + DemoSetup, + DemoStart, +} +``` + +Great! So you have your plugin definition, and you've moved all of your static exports to the top level of your plugin... now let's move on to the runtime contract your plugin will be exposing. + +#### 3. Export your runtime contract + +Next, we need a way to expose your runtime dependencies. In the new platform, core will handle this for you. But while we are still in the legacy world, other plugins will need a way to consume your plugin's contract without the help of core. + +So we will take a similar approach to what was described above in the server section: actually call the `Plugin.setup()` and `Plugin.start()` methods, and export the values those return for other legacy plugins to consume. By convention, we've been placing this in a `legacy.ts` file, which also serves as our shim where we import our legacy dependencies and reshape them into what we are expecting in the new platform: + +```ts +// public/legacy.ts +import { PluginInitializerContext } from '../../../../core/public'; +import { npSetup, npStart } from 'ui/new_platform'; +import { plugin } from '.'; + +import { setup as fooSetup, start as fooStart } from '../../foo/public/legacy'; // assumes `foo` lives in `legacy/core_plugins` + +const pluginInstance = plugin({} as PluginInitializerContext); +const shimCoreSetup = { + ...npSetup.core, + bar: {}, // shim for a core service that hasn't migrated yet +}; +const shimCoreStart = { + ...npStart.core, + bar: {}, +}; +const shimSetupPlugins = { + ...npSetup.plugins, + foo: fooSetup, +}; +const shimStartPlugins = { + ...npStart.plugins, + foo: fooStart, +}; + +export const setup = pluginInstance.setup(shimCoreSetup, shimSetupPlugins); +export const start = pluginInstance.start(shimCoreStart, shimStartPlugins); +``` + +> As you build your shims, you may be wondering where you will find some legacy services in the new platform. Skip to [the tables below](#how-do-i-build-my-shim-for-new-platform-services) for a list of some of the more common legacy services and where we currently expect them to live. + +Notice how in the example above, we are importing the `setup` and `start` contracts from the legacy shim provided by `foo` plugin; we could just as easily be importing modules from `ui/public` here as well. + +The point is that, over time, this becomes the one file in our plugin containing stateful imports from the legacy world. And _that_ is where things start to get interesting... + +#### 4. Move "owned" UI modules into your plugin and expose them from your public contract Everything inside of the `ui/public` directory is going to be dealt with in one of the following ways: @@ -621,7 +767,20 @@ Concerns around ownership or duplication of a given module should be raised and A great outcome is a module being deleted altogether because it isn't used or it was used so lightly that it was easy to refactor away. -### Provide plugin extension points decoupled from angular.js +If it is determined that your plugin is going to own any UI modules that other plugins depend on, you'll want to migrate these quickly so that there's time for downstream plugins to update their imports. This will ultimately involve moving the module code into your plugin, and exposing it via your setup/start contracts, or as static code from your `plugin/index.ts`. We have identified owners for most of the legacy UI modules; if you aren't sure where you should move something that you own, please consult with the platform team. + +Depending on the module's level of complexity and the number of other places in Kibana that rely on it, there are a number of strategies you could use for this: + +* **Do it all at once.** Move the code, expose it from your plugin, and update all imports across Kibana. + - This works best for small pieces of code that aren't widely used. +* **Shim first, move later.** Expose the code from your plugin by importing it in your shim and then re-exporting it from your plugin first, then gradually update imports to pull from the new location, leaving the actual moving of the code as a final step. + - This works best for the largest, most widely used modules that would otherwise result in huge, hard-to-review PRs. + - It makes things easier by splitting the process into small, incremental PRs, but is probably overkill for things with a small surface area. +* **Hybrid approach.** As a middle ground, you can also move the code to your plugin immediately, and then re-export your plugin code from the original `ui/public` directory. + - This eliminates any concerns about backwards compatibility by allowing you to update the imports across Kibana later. + - Works best when the size of the PR is such that moving the code can be done without much refactoring. + +#### 5. Provide plugin extension points decoupled from angular.js There will be no global angular module in the new platform, which means none of the functionality provided by core will be coupled to angular. Since there is no global angular module shared by all applications, plugins providing extension points to be used by other plugins can not couple those extension points to angular either. @@ -633,7 +792,7 @@ Another way to address this problem is to create an entirely new set of plugin A Please talk with the platform team when formalizing _any_ client-side extension points that you intend to move to the new platform as there are some bundling considerations to consider. -### Move all webpack alias imports into uiExport entry files +#### 6. Move all webpack alias imports into uiExport entry files Existing plugins import three things using webpack aliases today: services from ui/public (`ui/`), services from other plugins (`plugins/`), and uiExports themselves (`uiExports/`). These webpack aliases will not exist once we remove the legacy plugin system, so part of our migration effort is addressing all of the places where they are used today. @@ -643,7 +802,29 @@ With the legacy plugin system, extensions of core and other plugins are handled Each uiExport path is an entry file into one specific set of functionality provided by a client-side plugin. All webpack alias-based imports should be moved to these entry files, where they are appropriate. Moving a deeply nested webpack alias-based import in a plugin to one of the uiExport entry files might require some refactoring to ensure the dependency is now passed down to the appropriate place as function arguments instead of via import statements. -### Switch to new platform services +For stateful dependencies using the `plugins/` and `ui/` webpack aliases, you should be able to take advantage of the `legacy.ts` shim you created earlier. By placing these imports directly in your shim, you can pass the dependencies you need into your `Plugin.start` and `Plugin.setup` methods, from which point they can be passed down to the rest of your plugin's entry files. + +For items that don't yet have a clear "home" in the new platform, it may also be helpful to somehow indicate this in your shim to make it easier to remember that you'll need to change this later. One convention we've found helpful for this is simply using a namespace like `__LEGACY`: + +```ts +// public/legacy.ts +import { uiThing } from 'ui/thing'; +... + +const pluginInstance = plugin({} as PluginInitializerContext); +const shimSetupPlugins = { + ...npSetup.plugins, + foo: fooSetup, + __LEGACY: { + uiThing, // eventually this will move out of __LEGACY and into a proper plugin + }, +}; + +... +export const setup = pluginInstance.setup(npSetup.core, shimSetupPlugins); +``` + +#### 7. Switch to new platform services At this point, your plugin has one or more uiExport entry files that together contain all of the webpack alias-based import statements needed to run your plugin. Each one of these import statements is either a service that is or will be provided by core or a service provided by another plugin. @@ -651,14 +832,20 @@ As new non-angular-based APIs are added, update your entry files to import the c Once all of the existing webpack alias-based imports in your plugin switch to `ui/new_platform`, it no longer depends directly on the legacy "core" features or other legacy plugins, so it is ready to officially migrate to the new platform. -### Migrate to the new plugin system +#### 8. Migrate to the new plugin system With all of your services converted, you are now ready to complete your migration to the new platform. -Many plugins at this point will create a new plugin definition class and copy and paste the code from their various uiExport entry files directly into the new plugin class. The legacy uiExport entry files can then simply be deleted. +Many plugins at this point will copy over their plugin definition class & the code from their various service/uiExport entry files directly into the new plugin directory. The `legacy.ts` shim file can then simply be deleted. With the previous steps resolved, this final step should be easy, but the exact process may vary plugin by plugin, so when you're at this point talk to the platform team to figure out the exact changes you need. +#### Bonus: Tips for complex migration scenarios + +For a few plugins, some of these steps (such as angular removal) could be a months-long process. In those cases, it may be helpful from an organizational perspective to maintain a clear separation of code that is and isn't "ready" for the new platform. + +One convention that is useful for this is creating a dedicated `public/np_ready` directory to house the code that is ready to migrate, and gradually move more and more code into it until the rest of your plugin is essentially empty. At that point, you'll be able to copy your `index.ts`, `plugin.ts`, and the contents of `./np_ready` over into your plugin in the new platform, leaving your legacy shim behind. This carries the added benefit of providing a way for us to introduce helpful tooling in the future, such as [custom eslint rules](https://github.com/elastic/kibana/pull/40537), which could be run against that specific directory to ensure your code is ready to migrate. + ## Frequently asked questions ### Is migrating a plugin an all-or-nothing thing? @@ -859,7 +1046,7 @@ import { npStart: { core } } from 'ui/new_platform'; _See also: [Public's CoreStart API Docs](/docs/development/core/public/kibana-plugin-public.corestart.md)_ ##### Plugins for shared application services -In client code, we have a series of plugins which house shared application services that are being built in the shape of the new platform, but still technically reside in the legacy world. If your plugin depends on any of the APIs below, you'll need to add a dependency on the new platform plugin which will house them moving forward. +In client code, we have a series of plugins which house shared application services that are being built in the shape of the new platform, but for the time being, are only available in legacy. So if your plugin depends on any of the APIs below, you'll need build your plugin as a legacy plugin that shims the new platform. Once these API's have been moved to the new platform you can migrate your plugin and declare a dependency on the plugin that owns the API's you require. The contracts for these plugins are exposed for you to consume in your own plugin; we have created dedicated exports for the `setup` and `start` contracts in a file called `legacy`. By passing these contracts to your plugin's `setup` and `start` methods, you can mimic the functionality that will eventually be provided in the new platform. @@ -869,15 +1056,14 @@ import { setup, start } from '../core_plugins/embeddables/public/legacy'; import { setup, start } from '../core_plugins/visualizations/public/legacy'; ``` -| Legacy Platform | New Platform | Notes | -|--------------------------------------------------------|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| -| `core_plugins/interpreter` | `data.expressions` | still in progress | -| `import 'ui/apply_filters'` | `data.filter.loadLegacyDirectives` | `loadLegacyDirectives()` should be called explicitly where you previously relied on importing for side effects | -| `import 'ui/filter_bar'` | `data.filter.loadLegacyDirectives` | `loadLegacyDirectives()` should be called explicitly where you previously relied on importing for side effects | -| `import 'ui/query_bar'` | `data.query.loadLegacyDirectives` | `loadLegacyDirectives()` should be called explicitly where you previously relied on importing for side effects | -| `import 'ui/search_bar'` | `data.search.loadLegacyDirectives` | `loadLegacyDirectives()` should be called explicitly where you previously relied on importing for side effects | -| `import { QueryBar } from 'ui/query_bar'` | `data.query.ui.QueryBar` | -- | -| `import { SearchBar } from 'ui/search_bar'` | `data.search.ui.SearchBar` | -- | +| Legacy Platform | New Platform | Notes | +|--------------------------------------------------------|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | +| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | +| `import 'ui/query_bar'` | `import { QueryBar, QueryBarInput } from '../data/public'` | Directives are deprecated. | +| `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | +| `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../kibana_react/public'` | Directive is still available in `ui/kbn_top_nav`. | +| `core_plugins/interpreter` | `data.expressions` | still in progress | | `ui/courier` | `data.search` | still in progress | | `ui/embeddable` | `embeddables` | still in progress | | `ui/filter_manager` | `data.filter` | -- | @@ -928,20 +1114,37 @@ class MyPlugin { } ``` -### Mock core services in tests -Core services already provide mocks to simplify testing and make sure plugins always rely on valid public contracts. +### Mock new platform services in tests + +#### Writing mocks for your plugin +Core services already provide mocks to simplify testing and make sure plugins always rely on valid public contracts: ```typescript // my_plugin/server/plugin.test.ts -Import { configServiceMock } from 'src/core/server/mocks.ts' +import { configServiceMock } from 'src/core/server/mocks'; const configService = configServiceMock.create(); configService.atPath.mockReturnValue(config$); … -const plugin = new MyPlugin({ configService }, …) +const plugin = new MyPlugin({ configService }, …); ``` -However it's not mandatory, we strongly recommended to export your plugin mocks as well, in order for dependent plugins to use them in tests. Your plugin mocks should be exported from the root level of the plugin. Plugin mocks should consist of mocks for *public API only*: setup/start/stop contracts. Mocks aren't necessary for pure functions as other plugins can call original implementation in tests. + +Or if you need to get the whole core `setup` or `start` contracts: ```typescript -// my_plugin/server/mocks.ts +// my_plugin/public/plugin.test.ts +import { coreMock } from 'src/core/public/mocks'; + +const coreSetup = coreMock.createSetup(); +coreSetup.uiSettings.get.mockImplementation((key: string) => { + … +}); +… +const plugin = new MyPlugin(coreSetup, ...); +``` + + +Although it isn't mandatory, we strongly recommended you export your plugin mocks as well, in order for dependent plugins to use them in tests. Your plugin mocks should be exported from the root `/server` and `/public` directories in your plugin: +```typescript +// my_plugin/server/mocks.ts or my_plugin/public/mocks.ts const createSetupContractMock = () => { const startContract: jest.Mocked= { isValid: jest.fn(); @@ -953,6 +1156,31 @@ const createSetupContractMock = () => { export const myPluginMocks = { createSetup: createSetupContractMock, - createStart: ... + createStart: … } ``` +Plugin mocks should consist of mocks for *public APIs only*: setup/start/stop contracts. Mocks aren't necessary for pure functions as other plugins can call the original implementation in tests. + +#### Using mocks in your tests +During the migration process, it is likely you are preparing your plugin by shimming in new platform-ready dependencies via the legacy `ui/new_platform` module: +```typescript +import { npSetup, npStart } from 'ui/new_platform'; +``` + +If you are using this approach, the easiest way to mock core and new platform-ready plugins in your legacy tests is to mock the `ui/new_platform` module: +```typescript +jest.mock('ui/new_platform'); +``` + +This will automatically mock the services in `ui/new_platform` thanks to the [helpers that have been added](https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/new_platform/__mocks__/helpers.ts) to that module. + +If others are consuming your plugin's new platform contracts via the `ui/new_platform` module, you'll want to update the helpers as well to ensure your contracts are properly mocked. + +> Note: The `ui/new_platform` mock is only designed for use by old Jest tests. If you are writing new tests, you should structure your code and tests such that you don't need this mock. Instead, you should import the `core` mock directly and instantiate it. + +#### What about karma tests? +While our plan is to only provide first-class mocks for Jest tests, there are many legacy karma tests that cannot be quickly or easily converted to Jest -- particularly those which are still relying on mocking Angular services via `ngMock`. + +For these tests, we are maintaining a separate set of mocks. Files with a `.karma_mock.{js|ts|tsx}` extension will be loaded _globally_ before karma tests are run. + +It is important to note that this behavior is different from `jest.mock('ui/new_platform')`, which only mocks tests on an individual basis. If you encounter any failures in karma tests as a result of new platform migration efforts, you may need to add a `.karma_mock.js` file for the affected services, or add to the existing karma mock we are maintaining in `ui/new_platform`. diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index 872980154544..85d997f3dc9a 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -28,7 +28,6 @@ const createSetupContractMock = (): jest.Mocked => ({ }); const createStartContractMock = (): jest.Mocked => ({ - mount: jest.fn(), ...capabilitiesServiceMock.createStartContract(), }); diff --git a/src/core/public/application/application_service.test.tsx b/src/core/public/application/application_service.test.tsx index af100ab5f5f5..d2266671367a 100644 --- a/src/core/public/application/application_service.test.tsx +++ b/src/core/public/application/application_service.test.tsx @@ -28,11 +28,16 @@ describe('#start()', () => { setup.registerApp({ id: 'app1' } as any); setup.registerLegacyApp({ id: 'app2' } as any); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - expect((await service.start({ injectedMetadata })).availableApps).toMatchInlineSnapshot(` + const startContract = await service.start({ injectedMetadata }); + expect(startContract.availableApps).toMatchInlineSnapshot(` Array [ Object { "id": "app1", }, +] +`); + expect(startContract.availableLegacyApps).toMatchInlineSnapshot(` +Array [ Object { "id": "app2", }, @@ -48,6 +53,7 @@ Array [ await service.start({ injectedMetadata }); expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ apps: [{ id: 'app1' }], + legacyApps: [], injectedMetadata, }); }); @@ -59,7 +65,8 @@ Array [ const injectedMetadata = injectedMetadataServiceMock.createStartContract(); await service.start({ injectedMetadata }); expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ - apps: [{ id: 'legacyApp1' }], + apps: [], + legacyApps: [{ id: 'legacyApp1' }], injectedMetadata, }); }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index e7f18cf7bfcd..528b81ad40be 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -18,8 +18,9 @@ */ import { Observable, BehaviorSubject } from 'rxjs'; -import { CapabilitiesStart, CapabilitiesService, Capabilities } from './capabilities'; +import { CapabilitiesService, Capabilities } from './capabilities'; import { InjectedMetadataStart } from '../injected_metadata'; +import { RecursiveReadonly } from '../../utils'; interface BaseApp { id: string; @@ -59,11 +60,6 @@ interface BaseApp { /** @public */ export interface App extends BaseApp { - /** - * The root route to mount this application at. - */ - rootRoute: string; - /** * A mount function called when the user navigates to this app's `rootRoute`. * @param targetDomElement An HTMLElement to mount the application onto. @@ -98,10 +94,27 @@ export interface ApplicationSetup { registerLegacyApp(app: LegacyApp): void; } +/** + * @public + */ export interface ApplicationStart { - mount: (mountHandler: Function) => void; - availableApps: CapabilitiesStart['availableApps']; - capabilities: CapabilitiesStart['capabilities']; + /** + * Gets the read-only capabilities. + */ + capabilities: RecursiveReadonly; + + /** + * Apps available based on the current capabilities. Should be used + * to show navigation links and make routing decisions. + */ + availableApps: readonly App[]; + + /** + * Apps available based on the current capabilities. Should be used + * to show navigation links and make routing decisions. + * @internal + */ + availableLegacyApps: readonly LegacyApp[]; } interface StartDeps { @@ -132,17 +145,11 @@ export class ApplicationService { this.apps$.complete(); this.legacyApps$.complete(); - const apps = [...this.apps$.value, ...this.legacyApps$.value]; - const { capabilities, availableApps } = await this.capabilities.start({ - apps, + return this.capabilities.start({ + apps: this.apps$.value, + legacyApps: this.legacyApps$.value, injectedMetadata, }); - - return { - mount() {}, - capabilities, - availableApps, - }; } public stop() {} diff --git a/src/core/public/application/capabilities/capabilities_service.mock.ts b/src/core/public/application/capabilities/capabilities_service.mock.ts index b6f5323d1907..71b069fd8043 100644 --- a/src/core/public/application/capabilities/capabilities_service.mock.ts +++ b/src/core/public/application/capabilities/capabilities_service.mock.ts @@ -18,12 +18,14 @@ */ import { CapabilitiesService, CapabilitiesStart } from './capabilities_service'; import { deepFreeze } from '../../../utils/'; -import { MixedApp } from '../application_service'; +import { App, LegacyApp } from '../application_service'; const createStartContractMock = ( - apps: readonly MixedApp[] = [] + apps: readonly App[] = [], + legacyApps: readonly LegacyApp[] = [] ): jest.Mocked => ({ availableApps: apps, + availableLegacyApps: legacyApps, capabilities: deepFreeze({ catalogue: {}, management: {}, @@ -33,7 +35,9 @@ const createStartContractMock = ( type CapabilitiesServiceContract = PublicMethodsOf; const createMock = (): jest.Mocked => ({ - start: jest.fn().mockImplementation(({ apps }) => createStartContractMock(apps)), + start: jest + .fn() + .mockImplementation(({ apps, legacyApps }) => createStartContractMock(apps, legacyApps)), }); export const capabilitiesServiceMock = { diff --git a/src/core/public/application/capabilities/capabilities_service.test.ts b/src/core/public/application/capabilities/capabilities_service.test.ts index d7d3c4c7ef72..1c60c1eeb195 100644 --- a/src/core/public/application/capabilities/capabilities_service.test.ts +++ b/src/core/public/application/capabilities/capabilities_service.test.ts @@ -30,25 +30,33 @@ describe('#start', () => { navLinks: { app1: true, app2: false, + legacyApp1: true, + legacyApp2: false, }, foo: { feature: true }, bar: { feature: true }, }, } as any, }).start(); + const apps = [{ id: 'app1' }, { id: 'app2', capabilities: { app2: { feature: true } } }] as any; + const legacyApps = [ + { id: 'legacyApp1' }, + { id: 'legacyApp2', capabilities: { app2: { feature: true } } }, + ] as any; it('filters available apps based on returned navLinks', async () => { const service = new CapabilitiesService(); - expect((await service.start({ apps, injectedMetadata })).availableApps).toEqual([ - { id: 'app1' }, - ]); + const startContract = await service.start({ apps, legacyApps, injectedMetadata }); + expect(startContract.availableApps).toEqual([{ id: 'app1' }]); + expect(startContract.availableLegacyApps).toEqual([{ id: 'legacyApp1' }]); }); it('does not allow Capabilities to be modified', async () => { const service = new CapabilitiesService(); const { capabilities } = await service.start({ apps, + legacyApps, injectedMetadata, }); diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index a341ed5e1417..51c5a218e70b 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -18,11 +18,12 @@ */ import { deepFreeze, RecursiveReadonly } from '../../../utils'; -import { MixedApp } from '../application_service'; +import { LegacyApp, App } from '../application_service'; import { InjectedMetadataStart } from '../../injected_metadata'; interface StartDeps { - apps: readonly MixedApp[]; + apps: readonly App[]; + legacyApps: readonly LegacyApp[]; injectedMetadata: InjectedMetadataStart; } @@ -49,35 +50,28 @@ export interface Capabilities { [key: string]: Record>; } -/** - * Capabilities Setup. - * @public - */ +/** @internal */ export interface CapabilitiesStart { - /** - * Gets the read-only capabilities. - */ capabilities: RecursiveReadonly; - - /** - * Apps available based on the current capabilities. Should be used - * to show navigation links and make routing decisions. - */ - availableApps: readonly MixedApp[]; + availableApps: readonly App[]; + availableLegacyApps: readonly LegacyApp[]; } -/** @internal */ - /** * Service that is responsible for UI Capabilities. + * @internal */ export class CapabilitiesService { - public async start({ apps, injectedMetadata }: StartDeps): Promise { + public async start({ + apps, + legacyApps, + injectedMetadata, + }: StartDeps): Promise { const capabilities = deepFreeze(injectedMetadata.getCapabilities()); - const availableApps = apps.filter(app => capabilities.navLinks[app.id]); return { - availableApps, + availableApps: apps.filter(app => capabilities.navLinks[app.id]), + availableLegacyApps: legacyApps.filter(app => capabilities.navLinks[app.id]), capabilities, }; } diff --git a/src/core/public/application/capabilities/index.ts b/src/core/public/application/capabilities/index.ts index 4cabc3770ec2..9d8bec955eb9 100644 --- a/src/core/public/application/capabilities/index.ts +++ b/src/core/public/application/capabilities/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Capabilities, CapabilitiesService, CapabilitiesStart } from './capabilities_service'; +export { Capabilities, CapabilitiesService } from './capabilities_service'; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 35ac102c7a40..74f2a09b895d 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -26,7 +26,7 @@ import { } from './chrome_service'; const createStartContractMock = () => { - const startContract: jest.Mocked = { + const startContract: DeeplyMockedKeys = { getComponent: jest.fn(), navLinks: { getNavLinks$: jest.fn(), @@ -66,6 +66,7 @@ const createStartContractMock = () => { getHelpExtension$: jest.fn(), setHelpExtension: jest.fn(), }; + startContract.navLinks.getAll.mockReturnValue([]); startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false)); startContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false)); diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index 055343a50039..b323bf5318b2 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -28,29 +28,6 @@ export interface ChromeNavLink { */ readonly id: string; - /** - * Indicates whether or not this app is currently on the screen. - * - * NOTE: remove this when ApplicationService is implemented and managing apps. - */ - readonly active?: boolean; - - /** - * Disables a link from being clickable. - * - * NOTE: 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. - */ - readonly disabled?: boolean; - - /** - * Hides a link from the navigation. - * - * NOTE: remove this when ApplicationService is implemented. Instead, plugins should only - * register an Application if needed. - */ - readonly hidden?: boolean; - /** * An ordinal used to sort nav links relative to one another for display. */ @@ -62,14 +39,14 @@ export interface ChromeNavLink { readonly title: string; /** - * A tooltip shown when hovering over an app link. + * The base route used to open the root of an application. */ - readonly tooltip?: string; + readonly baseUrl: string; /** - * The base route used to open the root of an application. + * A tooltip shown when hovering over an app link. */ - readonly baseUrl: string; + readonly tooltip?: string; /** * A EUI iconType that will be used for the app's icon. This icon @@ -88,25 +65,70 @@ export interface ChromeNavLink { /** * A url base that legacy apps can set to match deep URLs to an applcation. * - * NOTE: this should be removed once legacy apps are gone. + * @internalRemarks + * This should be removed once legacy apps are gone. + * + * @deprecated */ readonly subUrlBase?: string; /** * Whether or not the subUrl feature should be enabled. * - * NOTE: only read by legacy platform. + * @internalRemarks + * Only read by legacy platform. + * + * @deprecated */ readonly linkToLastSubUrl?: boolean; /** * A url that legacy apps can set to deep link into their applications. * - * NOTE: Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should + * @internalRemarks + * Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should * be removed once the ApplicationService is implemented and mounting apps. At that * time, each app can handle opening to the previous location when they are mounted. + * + * @deprecated */ readonly url?: string; + + /** + * 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; + + /** + * Disables a link from being clickable. + * + * @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 */ 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 fe74ae3a7d9a..dfef8dc7989f 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 @@ -21,10 +21,17 @@ import { NavLinksService } from './nav_links_service'; import { take, map, takeLast } from 'rxjs/operators'; const mockAppService = { - availableApps: [ - { id: 'app1', order: 0, title: 'App 1', icon: 'app1', rootRoute: '/app1' }, - { id: 'app2', order: -10, title: 'App 2', euiIconType: 'canvasApp', rootRoute: '/app2' }, - { id: 'legacyApp', order: 20, title: 'Legacy App', appUrl: '/legacy-app' }, + availableApps: [], + availableLegacyApps: [ + { id: 'legacyApp1', order: 0, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' }, + { + id: 'legacyApp2', + order: -10, + title: 'Legacy App 2', + euiIconType: 'canvasApp', + appUrl: '/app2', + }, + { id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3' }, ], } as any; @@ -53,17 +60,20 @@ describe('NavLinksService', () => { map(links => links.map(l => l.id)) ) .toPromise() - ).toEqual(['app2', 'app1', 'legacyApp']); + ).toEqual(['legacyApp2', 'legacyApp1', 'legacyApp3']); }); 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('app1', { active: true }); + start.update('legacyApp1', { active: true }); service.stop(); - expect(emittedLinks).toEqual([['app2', 'app1', 'legacyApp'], ['app2', 'app1', 'legacyApp']]); + expect(emittedLinks).toEqual([ + ['legacyApp2', 'legacyApp1', 'legacyApp3'], + ['legacyApp2', 'legacyApp1', 'legacyApp3'], + ]); }); it('completes when service is stopped', async () => { @@ -78,7 +88,7 @@ describe('NavLinksService', () => { describe('#get()', () => { it('returns link if exists', () => { - expect(start.get('app1')!.title).toEqual('App 1'); + expect(start.get('legacyApp1')!.title).toEqual('Legacy App 1'); }); it('returns undefined if it does not exist', () => { @@ -88,13 +98,13 @@ describe('NavLinksService', () => { describe('#getAll()', () => { it('returns a sorted array of navlinks', () => { - expect(start.getAll().map(l => l.id)).toEqual(['app2', 'app1', 'legacyApp']); + expect(start.getAll().map(l => l.id)).toEqual(['legacyApp2', 'legacyApp1', 'legacyApp3']); }); }); describe('#has()', () => { it('returns true if exists', () => { - expect(start.has('app1')).toBe(true); + expect(start.has('legacyApp1')).toBe(true); }); it('returns false if it does not exist', () => { @@ -113,11 +123,11 @@ describe('NavLinksService', () => { map(links => links.map(l => l.id)) ) .toPromise() - ).toEqual(['app2', 'app1', 'legacyApp']); + ).toEqual(['legacyApp2', 'legacyApp1', 'legacyApp3']); }); it('removes all other links', async () => { - start.showOnly('app1'); + start.showOnly('legacyApp1'); expect( await start .getNavLinks$() @@ -126,23 +136,24 @@ describe('NavLinksService', () => { map(links => links.map(l => l.id)) ) .toPromise() - ).toEqual(['app1']); + ).toEqual(['legacyApp1']); }); }); describe('#update()', () => { it('updates the navlinks and returns the updated link', async () => { - expect(start.update('app1', { hidden: true })).toMatchInlineSnapshot(` -Object { - "baseUrl": "http://localhost/wow/app1", - "hidden": true, - "icon": "app1", - "id": "app1", - "order": 0, - "rootRoute": "/app1", - "title": "App 1", -} -`); + expect(start.update('legacyApp1', { hidden: true })).toMatchInlineSnapshot(` + Object { + "appUrl": "/app1", + "baseUrl": "http://localhost/wow/app1", + "hidden": true, + "icon": "legacyApp1", + "id": "legacyApp1", + "legacy": true, + "order": 0, + "title": "Legacy App 1", + } + `); const hiddenLinkIds = await start .getNavLinks$() .pipe( @@ -150,7 +161,7 @@ Object { map(links => links.filter(l => l.hidden).map(l => l.id)) ) .toPromise(); - expect(hiddenLinkIds).toEqual(['app1']); + expect(hiddenLinkIds).toEqual(['legacyApp1']); }); it('returns undefined if link does not exist', () => { diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index 8dd5525db29d..2250ec40f0f4 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -99,20 +99,20 @@ export class NavLinksService { private readonly stop$ = new ReplaySubject(1); public start({ application, http }: StartDeps): ChromeNavLinks { + const legacyAppLinks = application.availableLegacyApps.map( + app => + [ + app.id, + new NavLinkWrapper({ + ...app, + legacy: true, + baseUrl: relativeToAbsolute(http.basePath.prepend(app.appUrl)), + }), + ] as [string, NavLinkWrapper] + ); + const navLinks$ = new BehaviorSubject>( - new Map( - application.availableApps.map( - app => - [ - app.id, - new NavLinkWrapper({ - ...app, - // Either rootRoute or appUrl must be defined. - baseUrl: relativeToAbsolute(http.basePath.prepend((app.rootRoute || app.appUrl)!)), - }), - ] as [string, NavLinkWrapper] - ) - ) + new Map(legacyAppLinks) ); const forceAppSwitcherNavigation$ = new BehaviorSubject(false); diff --git a/src/core/public/context/context_service.mock.ts b/src/core/public/context/context_service.mock.ts new file mode 100644 index 000000000000..eb55ced69dc0 --- /dev/null +++ b/src/core/public/context/context_service.mock.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ContextService, ContextSetup } from './context_service'; +import { contextMock } from '../../utils/context.mock'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + createContextContainer: jest.fn().mockImplementation(() => contextMock.create()), + }; + return setupContract; +}; + +type ContextServiceContract = PublicMethodsOf; +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + }; + mocked.setup.mockReturnValue(createSetupContractMock()); + return mocked; +}; + +export const contextServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/public/context/context_service.test.mocks.ts b/src/core/public/context/context_service.test.mocks.ts new file mode 100644 index 000000000000..5cf492d97aaf --- /dev/null +++ b/src/core/public/context/context_service.test.mocks.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 { contextMock } from '../../utils/context.mock'; + +export const MockContextConstructor = jest.fn(contextMock.create); +jest.doMock('../../utils/context', () => ({ + ContextContainer: MockContextConstructor, +})); diff --git a/src/core/public/context/context_service.test.ts b/src/core/public/context/context_service.test.ts new file mode 100644 index 000000000000..d575d57a6b27 --- /dev/null +++ b/src/core/public/context/context_service.test.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 { PluginOpaqueId } from '../../server'; +import { MockContextConstructor } from './context_service.test.mocks'; +import { ContextService } from './context_service'; + +const pluginDependencies = new Map(); + +describe('ContextService', () => { + describe('#setup()', () => { + test('createContextContainer returns a new container configured with pluginDependencies', () => { + const coreId = Symbol(); + const service = new ContextService({ coreId }); + const setup = service.setup({ pluginDependencies }); + expect(setup.createContextContainer()).toBeDefined(); + expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, coreId); + }); + }); +}); diff --git a/src/core/public/context/context_service.ts b/src/core/public/context/context_service.ts new file mode 100644 index 000000000000..704524d83863 --- /dev/null +++ b/src/core/public/context/context_service.ts @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { PluginOpaqueId } from '../../server'; +import { IContextContainer, ContextContainer } from '../../utils/context'; +import { CoreContext } from '../core_system'; + +interface StartDeps { + pluginDependencies: ReadonlyMap; +} + +/** @internal */ +export class ContextService { + constructor(private readonly core: CoreContext) {} + + public setup({ pluginDependencies }: StartDeps): ContextSetup { + return { + createContextContainer: < + TContext extends {}, + THandlerReturn, + THandlerParameters extends any[] = [] + >() => + new ContextContainer( + pluginDependencies, + this.core.coreId + ), + }; + } +} + +/** + * {@inheritdoc IContextContainer} + * + * @example + * Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we + * want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. + * ```ts + * export interface VizRenderContext { + * core: { + * i18n: I18nStart; + * uiSettings: UISettingsClientContract; + * } + * [contextName: string]: unknown; + * } + * + * export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; + * + * class VizRenderingPlugin { + * private readonly vizRenderers = new Map () => void)>(); + * + * constructor(private readonly initContext: PluginInitializerContext) {} + * + * setup(core) { + * this.contextContainer = core.context.createContextContainer< + * VizRenderContext, + * ReturnType, + * [HTMLElement] + * >(); + * + * return { + * registerContext: this.contextContainer.registerContext, + * registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) => + * this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)), + * }; + * } + * + * start(core) { + * // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. + * this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ + * i18n: core.i18n, + * uiSettings: core.uiSettings + * })); + * + * return { + * registerContext: this.contextContainer.registerContext, + * + * renderVizualization: (renderMethod: string, domElement: HTMLElement) => { + * if (!this.vizRenderer.has(renderMethod)) { + * throw new Error(`Render method '${renderMethod}' has not been registered`); + * } + * + * // The handler can now be called directly with only an `HTMLElement` and will automatically + * // have a new `context` object created and populated by the context container. + * const handler = this.vizRenderers.get(renderMethod) + * return handler(domElement); + * } + * }; + * } + * } + * ``` + * + * @public + */ +export interface ContextSetup { + /** + * Creates a new {@link IContextContainer} for a service owner. + */ + createContextContainer< + TContext extends {}, + THandlerReturn, + THandlerParmaters extends any[] = [] + >(): IContextContainer; +} diff --git a/src/core/public/context/index.ts b/src/core/public/context/index.ts new file mode 100644 index 000000000000..28b2641b2a5a --- /dev/null +++ b/src/core/public/context/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 { ContextService, ContextSetup } from './context_service'; +export { IContextContainer, IContextProvider, IContextHandler } from '../../utils/context'; diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index 4a96214b3e5d..d2494badfacd 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -30,6 +30,7 @@ import { pluginsServiceMock } from './plugins/plugins_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { docLinksServiceMock } from './doc_links/doc_links_service.mock'; import { renderingServiceMock } from './rendering/rendering_service.mock'; +import { contextServiceMock } from './context/context_service.mock'; export const MockLegacyPlatformService = legacyPlatformServiceMock.create(); export const LegacyPlatformServiceConstructor = jest @@ -120,3 +121,9 @@ export const RenderingServiceConstructor = jest.fn().mockImplementation(() => Mo jest.doMock('./rendering', () => ({ RenderingService: RenderingServiceConstructor, })); + +export const MockContextService = contextServiceMock.create(); +export const ContextServiceConstructor = jest.fn().mockImplementation(() => MockContextService); +jest.doMock('./context', () => ({ + ContextService: ContextServiceConstructor, +})); diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 044a40b27599..7310a8f33eba 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -41,6 +41,7 @@ import { MockDocLinksService, MockRenderingService, RenderingServiceConstructor, + MockContextService, } from './core_system.test.mocks'; import { CoreSystem } from './core_system'; @@ -51,6 +52,7 @@ const defaultCoreSystemParams = { rootDomElement: document.createElement('div'), browserSupportsCsp: true, injectedMetadata: { + uiPlugins: [], csp: { warnLegacyBrowsers: true, }, @@ -160,6 +162,11 @@ describe('#setup()', () => { expect(MockApplicationService.setup).toHaveBeenCalledTimes(1); }); + it('calls context#setup()', async () => { + await setupCore(); + expect(MockContextService.setup).toHaveBeenCalledTimes(1); + }); + it('calls injectedMetadata#setup()', async () => { await setupCore(); expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index f3f466df8a78..7782c93c7bbb 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -19,6 +19,7 @@ import './core.css'; +import { CoreId } from '../server'; import { InternalCoreSetup, InternalCoreStart } from '.'; import { ChromeService } from './chrome'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors'; @@ -34,6 +35,8 @@ import { ApplicationService } from './application'; import { mapToObject } from '../utils/'; import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; +import { SavedObjectsService } from './saved_objects/saved_objects_service'; +import { ContextService } from './context'; interface Params { rootDomElement: HTMLElement; @@ -44,8 +47,9 @@ interface Params { } /** @internal */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CoreContext {} +export interface CoreContext { + coreId: CoreId; +} /** * The CoreSystem is the root of the new platform, and setups all parts @@ -61,6 +65,7 @@ export class CoreSystem { private readonly legacyPlatform: LegacyPlatformService; private readonly notifications: NotificationsService; private readonly http: HttpService; + private readonly savedObjects: SavedObjectsService; private readonly uiSettings: UiSettingsService; private readonly chrome: ChromeService; private readonly i18n: I18nService; @@ -69,6 +74,7 @@ export class CoreSystem { private readonly application: ApplicationService; private readonly docLinks: DocLinksService; private readonly rendering: RenderingService; + private readonly context: ContextService; private readonly rootDomElement: HTMLElement; private fatalErrorsSetup: FatalErrorsSetup | null = null; @@ -97,6 +103,7 @@ export class CoreSystem { this.notifications = new NotificationsService(); this.http = new HttpService(); + this.savedObjects = new SavedObjectsService(); this.uiSettings = new UiSettingsService(); this.overlay = new OverlayService(); this.application = new ApplicationService(); @@ -104,8 +111,9 @@ export class CoreSystem { this.docLinks = new DocLinksService(); this.rendering = new RenderingService(); - const core: CoreContext = {}; - this.plugins = new PluginsService(core); + const core: CoreContext = { coreId: Symbol('core') }; + this.context = new ContextService(core); + this.plugins = new PluginsService(core, injectedMetadata.uiPlugins); this.legacyPlatform = new LegacyPlatformService({ requireLegacyFiles, @@ -127,8 +135,12 @@ export class CoreSystem { const notifications = this.notifications.setup({ uiSettings }); const application = this.application.setup(); + const pluginDependencies = this.plugins.getOpaqueIds(); + const context = this.context.setup({ pluginDependencies }); + const core: InternalCoreSetup = { application, + context, fatalErrors: this.fatalErrorsSetup, http, injectedMetadata, @@ -157,6 +169,7 @@ export class CoreSystem { const injectedMetadata = await this.injectedMetadata.start(); const docLinks = await this.docLinks.start({ injectedMetadata }); const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); + const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); const application = await this.application.start({ injectedMetadata }); @@ -192,6 +205,7 @@ export class CoreSystem { chrome, docLinks, http, + savedObjects, i18n, injectedMetadata, notifications, diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index bcc94086e2a1..4ce84f8ab38d 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -46,8 +46,8 @@ const createServiceMock = (): ServiceSetupMockType => ({ removeAllInterceptors: jest.fn(), }); -const createSetupContractMock = () => createServiceMock(); -const createStartContractMock = () => createServiceMock(); +const createSetupContractMock = createServiceMock; +const createStartContractMock = createServiceMock; const createMock = () => { const mocked: jest.Mocked> = { diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index 7f33c23a8df2..0910635924ea 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -46,7 +46,7 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiSelectable.loadingOptions": "Loading options", "euiSelectable.noAvailableOptions": "There aren't any options available", "euiSelectable.noMatchingOptions": [Function], - "euiStat.loadingText": [Function], + "euiStat.loadingText": "Statistic is loading", "euiStep.completeStep": "Step", "euiStep.incompleteStep": "Incomplete Step", "euiStepHorizontal.buttonTitle": [Function], diff --git a/src/core/public/i18n/i18n_service.tsx b/src/core/public/i18n/i18n_service.tsx index 0be47c14e441..78411a7a418d 100644 --- a/src/core/public/i18n/i18n_service.tsx +++ b/src/core/public/i18n/i18n_service.tsx @@ -244,9 +244,9 @@ export class I18nService { values={{ searchValue }} /> ), - 'euiStat.loadingText': () => ( - - ), + 'euiStat.loadingText': i18n.translate('core.euiStat.loadingText', { + defaultMessage: 'Statistic is loading', + }), 'euiStep.completeStep': i18n.translate('core.euiStep.completeStep', { defaultMessage: 'Step', description: diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 2a88ebf86ab0..abc922ff97c1 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -50,7 +50,7 @@ import { ChromeRecentlyAccessedHistoryItem, } from './chrome'; import { FatalErrorsSetup, FatalErrorInfo } from './fatal_errors'; -import { HttpServiceBase, HttpSetup, HttpStart, HttpInterceptor } from './http'; +import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; import { InjectedMetadataSetup, InjectedMetadataStart, LegacyNavLink } from './injected_metadata'; import { @@ -62,13 +62,47 @@ import { ToastsApi, } from './notifications'; import { OverlayRef, OverlayStart } from './overlays'; -import { Plugin, PluginInitializer, PluginInitializerContext } from './plugins'; +import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; +import { SavedObjectsStart } from './saved_objects'; +import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } from './context'; export { CoreContext, CoreSystem } from './core_system'; export { RecursiveReadonly } from '../utils'; +export { + SavedObjectsBatchResponse, + SavedObjectsBulkCreateObject, + SavedObjectsBulkCreateOptions, + SavedObjectsCreateOptions, + SavedObjectsFindResponsePublic, + SavedObjectsUpdateOptions, + SavedObject, + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsBaseOptions, + SavedObjectsFindOptions, + SavedObjectsMigrationVersion, + SavedObjectsClientContract, + SavedObjectsClient, + SimpleSavedObject, +} from './saved_objects'; + +export { + HttpServiceBase, + HttpHeadersInit, + HttpRequestInit, + HttpFetchOptions, + HttpFetchQuery, + HttpErrorResponse, + HttpErrorRequest, + HttpInterceptor, + HttpResponse, + HttpHandler, + HttpBody, +} from './http'; /** * Core services exposed to the `Plugin` setup lifecycle @@ -80,6 +114,8 @@ export { RecursiveReadonly } from '../utils'; * https://github.com/Microsoft/web-build-tools/issues/1237 */ export interface CoreSetup { + /** {@link ContextSetup} */ + context: ContextSetup; /** {@link FatalErrorsSetup} */ fatalErrors: FatalErrorsSetup; /** {@link HttpSetup} */ @@ -108,6 +144,8 @@ export interface CoreStart { docLinks: DocLinksStart; /** {@link HttpStart} */ http: HttpStart; + /** {@link SavedObjectsStart} */ + savedObjects: SavedObjectsStart; /** {@link I18nStart} */ i18n: I18nStart; /** {@link NotificationsStart} */ @@ -146,12 +184,14 @@ export { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, ChromeStart, + IContextContainer, + IContextHandler, + IContextProvider, + ContextSetup, DocLinksStart, ErrorToastOptions, FatalErrorInfo, FatalErrorsSetup, - HttpInterceptor, - HttpServiceBase, HttpSetup, HttpStart, I18nStart, @@ -163,6 +203,8 @@ export { Plugin, PluginInitializer, PluginInitializerContext, + SavedObjectsStart, + PluginOpaqueId, Toast, ToastInput, ToastsApi, diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index a514bf715405..eb5b3e90f1a5 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -58,8 +58,11 @@ import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { LegacyPlatformService } from './legacy_service'; import { applicationServiceMock } from '../application/application_service.mock'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; +import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; +import { contextServiceMock } from '../context/context_service.mock'; const applicationSetup = applicationServiceMock.createSetupContract(); +const contextSetup = contextServiceMock.createSetupContract(); const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract(); const httpSetup = httpServiceMock.createSetupContract(); const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract(); @@ -75,6 +78,7 @@ const defaultParams = { const defaultSetupDeps = { core: { application: applicationSetup, + context: contextSetup, fatalErrors: fatalErrorsSetup, injectedMetadata: injectedMetadataSetup, notifications: notificationsSetup, @@ -93,6 +97,7 @@ const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); const uiSettingsStart = uiSettingsServiceMock.createStartContract(); +const savedObjectsStart = savedObjectsMock.createStartContract(); const defaultStartDeps = { core: { @@ -105,6 +110,7 @@ const defaultStartDeps = { notifications: notificationsStart, overlays: overlayStart, uiSettings: uiSettingsStart, + savedObjects: savedObjectsStart, }, targetDomElement: document.createElement('div'), plugins: {}, diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index b1312eaa228d..0f3a01c793ae 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -26,6 +26,8 @@ import { i18nServiceMock } from './i18n/i18n_service.mock'; import { notificationServiceMock } from './notifications/notifications_service.mock'; import { overlayServiceMock } from './overlays/overlay_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; +import { savedObjectsMock } from './saved_objects/saved_objects_service.mock'; +import { contextServiceMock } from './context/context_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { docLinksServiceMock } from './doc_links/doc_links_service.mock'; @@ -40,6 +42,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; function createCoreSetupMock() { const mock: MockedKeys = { + context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), notifications: notificationServiceMock.createSetupContract(), @@ -59,6 +62,7 @@ function createCoreStartMock() { notifications: notificationServiceMock.createStartContract(), overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), + savedObjects: savedObjectsMock.createStartContract(), }; return mock; diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts index 0f9dec0b311c..a9c44f63013c 100644 --- a/src/core/public/overlays/overlay_service.ts +++ b/src/core/public/overlays/overlay_service.ts @@ -77,6 +77,7 @@ export interface OverlayStart { openModal: ( modalChildren: React.ReactNode, modalProps?: { + className?: string; closeButtonAriaLabel?: string; 'data-test-subj'?: string; } diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index fc16b6b00456..8adcf8ca40c1 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -20,3 +20,4 @@ export * from './plugins_service'; export { Plugin, PluginInitializer } from './plugin'; export { PluginInitializerContext } from './plugin_context'; +export { PluginOpaqueId } from '../../server/types'; diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index bbe2baf006a8..6cbe0c7e0ed8 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -35,7 +35,8 @@ function createManifest( } let plugin: PluginWrapper>; -const initializerContext = {}; +const opaqueId = Symbol(); +const initializerContext = { opaqueId }; const addBasePath = (path: string) => path; beforeEach(() => { @@ -43,7 +44,7 @@ beforeEach(() => { mockPlugin.setup.mockClear(); mockPlugin.start.mockClear(); mockPlugin.stop.mockClear(); - plugin = new PluginWrapper(createManifest('plugin-a'), initializerContext); + plugin = new PluginWrapper(createManifest('plugin-a'), opaqueId, initializerContext); }); describe('PluginWrapper', () => { diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index a24c19e3219f..a8e52cc57cb6 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -17,7 +17,7 @@ * under the License. */ -import { DiscoveredPlugin } from '../../server'; +import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; import { loadPluginBundle } from './plugin_loader'; import { CoreStart, CoreSetup } from '..'; @@ -72,6 +72,7 @@ export class PluginWrapper< constructor( readonly discoveredPlugin: DiscoveredPlugin, + public readonly opaqueId: PluginOpaqueId, private readonly initializerContext: PluginInitializerContext ) { this.name = discoveredPlugin.id; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index bc77b139a86d..66cb7c4a1171 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -19,7 +19,7 @@ import { omit } from 'lodash'; -import { DiscoveredPlugin } from '../../server'; +import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { CoreContext } from '../core_system'; import { PluginWrapper } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; @@ -30,8 +30,12 @@ import { CoreSetup, CoreStart } from '../'; * * @public */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginInitializerContext {} +export interface PluginInitializerContext { + /** + * A symbol used to identify this plugin in the system. Needed when registering handlers or context providers. + */ + readonly opaqueId: PluginOpaqueId; +} /** * Provides a plugin-specific context passed to the plugin's construtor. This is currently @@ -43,9 +47,12 @@ export interface PluginInitializerContext {} */ export function createPluginInitializerContext( coreContext: CoreContext, + opaqueId: PluginOpaqueId, pluginManifest: DiscoveredPlugin ): PluginInitializerContext { - return {}; + return { + opaqueId, + }; } /** @@ -69,8 +76,9 @@ export function createPluginSetupContext< plugin: PluginWrapper ): CoreSetup { return { - http: deps.http, + context: omit(deps.context, 'setCurrentPlugin'), fatalErrors: deps.fatalErrors, + http: deps.http, notifications: deps.notifications, uiSettings: deps.uiSettings, }; @@ -107,5 +115,6 @@ export function createPluginStartContext< notifications: deps.notifications, overlays: deps.overlays, uiSettings: deps.uiSettings, + savedObjects: deps.savedObjects, }; } diff --git a/src/core/public/plugins/plugins_service.mock.ts b/src/core/public/plugins/plugins_service.mock.ts index 4df57b05fda3..900f20422b82 100644 --- a/src/core/public/plugins/plugins_service.mock.ts +++ b/src/core/public/plugins/plugins_service.mock.ts @@ -38,6 +38,7 @@ const createStartContractMock = () => { type PluginsServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { + getOpaqueIds: jest.fn(), setup: jest.fn(), start: jest.fn(), stop: jest.fn(), diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 55e91bde27cb..2b689e45b4f1 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -25,7 +25,7 @@ import { mockPluginInitializerProvider, } from './plugins_service.test.mocks'; -import { PluginName } from 'src/core/server'; +import { PluginName, DiscoveredPlugin } from 'src/core/server'; import { CoreContext } from '../core_system'; import { PluginsService, @@ -43,6 +43,8 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad import { httpServiceMock } from '../http/http_service.mock'; import { CoreSetup, CoreStart } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; +import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; +import { contextServiceMock } from '../context/context_service.mock'; export let mockPluginInitializers: Map; @@ -50,35 +52,37 @@ mockPluginInitializerProvider.mockImplementation( pluginName => mockPluginInitializers.get(pluginName)! ); +let plugins: Array<{ id: string; plugin: DiscoveredPlugin }>; + type DeeplyMocked = { [P in keyof T]: jest.Mocked }; -const mockCoreContext: CoreContext = {}; +const mockCoreContext: CoreContext = { coreId: Symbol() }; let mockSetupDeps: DeeplyMocked; let mockSetupContext: DeeplyMocked; let mockStartDeps: DeeplyMocked; let mockStartContext: DeeplyMocked; beforeEach(() => { + plugins = [ + { id: 'pluginA', plugin: createManifest('pluginA') }, + { id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) }, + { + id: 'pluginC', + plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }), + }, + ]; mockSetupDeps = { application: applicationServiceMock.createSetupContract(), - injectedMetadata: (function() { - const metadata = injectedMetadataServiceMock.createSetupContract(); - metadata.getPlugins.mockReturnValue([ - { id: 'pluginA', plugin: createManifest('pluginA') }, - { id: 'pluginB', plugin: createManifest('pluginB', { required: ['pluginA'] }) }, - { - id: 'pluginC', - plugin: createManifest('pluginC', { required: ['pluginA'], optional: ['nonexist'] }), - }, - ]); - return metadata; - })(), + context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), + injectedMetadata: injectedMetadataServiceMock.createSetupContract(), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), }; - mockSetupContext = omit(mockSetupDeps, 'application', 'injectedMetadata'); + mockSetupContext = { + ...omit(mockSetupDeps, 'application', 'injectedMetadata'), + }; mockStartDeps = { application: applicationServiceMock.createStartContract(), docLinks: docLinksServiceMock.createStartContract(), @@ -89,6 +93,7 @@ beforeEach(() => { notifications: notificationServiceMock.createStartContract(), overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), + savedObjects: savedObjectsMock.createStartContract(), }; mockStartContext = { ...omit(mockStartDeps, 'injectedMetadata'), @@ -148,10 +153,25 @@ function createManifest( }; } +test('`PluginsService.getOpaqueIds` returns dependency tree of symbols', () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); + expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` + Map { + Symbol(pluginA) => Array [], + Symbol(pluginB) => Array [ + Symbol(pluginA), + ], + Symbol(pluginC) => Array [ + Symbol(pluginA), + ], + } + `); +}); + test('`PluginsService.setup` fails if any bundle cannot be loaded', async () => { mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Could not load bundle"` ); @@ -159,14 +179,14 @@ test('`PluginsService.setup` fails if any bundle cannot be loaded', async () => test('`PluginsService.setup` fails if any plugin instance does not have a setup function', async () => { mockPluginInitializers.set('pluginA', (() => ({})) as any); - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"pluginA\\" does not define \\"setup\\" function."` ); }); test('`PluginsService.setup` calls loadPluginBundles with http and plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); @@ -175,17 +195,17 @@ test('`PluginsService.setup` calls loadPluginBundles with http and plugins', asy expect(mockLoadPluginBundle).toHaveBeenCalledWith(mockSetupDeps.http.basePath.prepend, 'pluginC'); }); -test('`PluginsService.setup` initalizes plugins with CoreContext', async () => { - const pluginsService = new PluginsService(mockCoreContext); +test('`PluginsService.setup` initalizes plugins with PluginIntitializerContext', async () => { + const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); - expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(mockCoreContext); - expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(mockCoreContext); - expect(mockPluginInitializers.get('pluginC')).toHaveBeenCalledWith(mockCoreContext); + expect(mockPluginInitializers.get('pluginA')).toHaveBeenCalledWith(expect.any(Object)); + expect(mockPluginInitializers.get('pluginB')).toHaveBeenCalledWith(expect.any(Object)); + expect(mockPluginInitializers.get('pluginC')).toHaveBeenCalledWith(expect.any(Object)); }); test('`PluginsService.setup` exposes dependent setup contracts to plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; @@ -203,15 +223,13 @@ test('`PluginsService.setup` exposes dependent setup contracts to plugins', asyn }); test('`PluginsService.setup` does not set missing dependent setup contracts', async () => { - mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([ - { id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) }, - ]); + plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; mockPluginInitializers.set('pluginD', jest.fn(() => ({ setup: jest.fn(), start: jest.fn(), })) as any); - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); // If a dependency is missing it should not be in the deps at all, not even as undefined. @@ -222,7 +240,7 @@ test('`PluginsService.setup` does not set missing dependent setup contracts', as }); test('`PluginsService.setup` returns plugin setup contracts', async () => { - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); const { contracts } = await pluginsService.setup(mockSetupDeps); // Verify that plugin contracts were available @@ -231,7 +249,7 @@ test('`PluginsService.setup` returns plugin setup contracts', async () => { }); test('`PluginsService.start` exposes dependent start contracts to plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); await pluginsService.start(mockStartDeps); @@ -250,15 +268,13 @@ test('`PluginsService.start` exposes dependent start contracts to plugins', asyn }); test('`PluginsService.start` does not set missing dependent start contracts', async () => { - mockSetupDeps.injectedMetadata.getPlugins.mockReturnValue([ - { id: 'pluginD', plugin: createManifest('pluginD', { required: ['missing'] }) }, - ]); + plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; mockPluginInitializers.set('pluginD', jest.fn(() => ({ setup: jest.fn(), start: jest.fn(), })) as any); - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); await pluginsService.start(mockStartDeps); @@ -270,7 +286,7 @@ test('`PluginsService.start` does not set missing dependent start contracts', as }); test('`PluginsService.start` returns plugin start contracts', async () => { - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); const { contracts } = await pluginsService.start(mockStartDeps); @@ -280,7 +296,7 @@ test('`PluginsService.start` returns plugin start contracts', async () => { }); test('`PluginService.stop` calls the stop function on each plugin', async () => { - const pluginsService = new PluginsService(mockCoreContext); + const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); const pluginAInstance = mockPluginInitializers.get('pluginA')!.mock.results[0].value; diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 03725a9d7f88..13a52d78d72f 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PluginName } from '../../server'; +import { DiscoveredPlugin, PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; import { PluginWrapper } from './plugin'; @@ -35,11 +35,11 @@ export type PluginsServiceStartDeps = InternalCoreStart; /** @internal */ export interface PluginsServiceSetup { - contracts: Map; + contracts: ReadonlyMap; } /** @internal */ export interface PluginsServiceStart { - contracts: Map; + contracts: ReadonlyMap; } /** @@ -50,37 +50,56 @@ export interface PluginsServiceStart { */ export class PluginsService implements CoreService { /** Plugin wrappers in topological order. */ - private readonly plugins: Map< - PluginName, - PluginWrapper> - > = new Map(); + private readonly plugins = new Map>>(); + private readonly pluginDependencies = new Map(); + private readonly satupPlugins: PluginName[] = []; - constructor(private readonly coreContext: CoreContext) {} + constructor( + private readonly coreContext: CoreContext, + plugins: Array<{ id: PluginName; plugin: DiscoveredPlugin }> + ) { + // Generate opaque ids + const opaqueIds = new Map(plugins.map(p => [p.id, Symbol(p.id)])); + + // Setup dependency map and plugin wrappers + plugins.forEach(({ id, plugin }) => { + // Setup map of dependencies + this.pluginDependencies.set(id, [ + ...plugin.requiredPlugins, + ...plugin.optionalPlugins.filter(optPlugin => opaqueIds.has(optPlugin)), + ]); - public async setup(deps: PluginsServiceSetupDeps) { - // Construct plugin wrappers, depending on the topological order set by the server. - deps.injectedMetadata - .getPlugins() - .forEach(({ id, plugin }) => - this.plugins.set( - id, - new PluginWrapper(plugin, createPluginInitializerContext(deps, plugin)) + // Construct plugin wrappers, depending on the topological order set by the server. + this.plugins.set( + id, + new PluginWrapper( + plugin, + opaqueIds.get(id)!, + createPluginInitializerContext(this.coreContext, opaqueIds.get(id)!, plugin) ) ); + }); + } + + public getOpaqueIds(): ReadonlyMap { + // Return dependency map of opaque ids + return new Map( + [...this.pluginDependencies].map(([id, deps]) => [ + this.plugins.get(id)!.opaqueId, + deps.map(depId => this.plugins.get(depId)!.opaqueId), + ]) + ); + } + public async setup(deps: PluginsServiceSetupDeps): Promise { // Load plugin bundles await this.loadPluginBundles(deps.http.basePath.prepend); // Setup each plugin with required and optional plugin contracts const contracts = new Map(); for (const [pluginName, plugin] of this.plugins.entries()) { - const pluginDeps = new Set([ - ...plugin.requiredPlugins, - ...plugin.optionalPlugins.filter(optPlugin => this.plugins.get(optPlugin)), - ]); - - const pluginDepContracts = [...pluginDeps.keys()].reduce( + const pluginDepContracts = [...this.pluginDependencies.get(pluginName)!].reduce( (depContracts, dependencyName) => { // Only set if present. Could be absent if plugin does not have client-side code or is a // missing optional plugin. @@ -108,16 +127,11 @@ export class PluginsService implements CoreService { // Setup each plugin with required and optional plugin contracts const contracts = new Map(); for (const [pluginName, plugin] of this.plugins.entries()) { - const pluginDeps = new Set([ - ...plugin.requiredPlugins, - ...plugin.optionalPlugins.filter(optPlugin => this.plugins.get(optPlugin)), - ]); - - const pluginDepContracts = [...pluginDeps.keys()].reduce( + const pluginDepContracts = [...this.pluginDependencies.get(pluginName)!].reduce( (depContracts, dependencyName) => { // Only set if present. Could be absent if plugin does not have client-side code or is a // missing optional plugin. diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 36c5ed84cd24..077dfac06de3 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -20,18 +20,12 @@ export interface ApplicationSetup { registerLegacyApp(app: LegacyApp): void; } -// Warning: (ae-missing-release-tag) "ApplicationStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface ApplicationStart { - // Warning: (ae-forgotten-export) The symbol "CapabilitiesStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) - availableApps: CapabilitiesStart['availableApps']; - // (undocumented) - capabilities: CapabilitiesStart['capabilities']; - // (undocumented) - mount: (mountHandler: Function) => void; + availableApps: readonly App[]; + // @internal + availableLegacyApps: readonly LegacyApp[]; + capabilities: RecursiveReadonly; } // @public @@ -95,18 +89,25 @@ export interface ChromeNavControls { // @public (undocumented) export interface ChromeNavLink { + // @deprecated readonly active?: boolean; readonly baseUrl: string; + // @deprecated readonly disabled?: boolean; readonly euiIconType?: string; readonly hidden?: boolean; 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; + // @deprecated readonly url?: string; } @@ -166,12 +167,23 @@ export interface ChromeStart { setIsVisible(isVisible: boolean): void; } +// @public +export interface ContextSetup { + createContextContainer(): IContextContainer; +} + // @internal (undocumented) export interface CoreContext { + // Warning: (ae-forgotten-export) The symbol "CoreId" needs to be exported by the entry point index.d.ts + // + // (undocumented) + coreId: CoreId; } // @public export interface CoreSetup { + // (undocumented) + context: ContextSetup; // (undocumented) fatalErrors: FatalErrorsSetup; // (undocumented) @@ -199,6 +211,8 @@ export interface CoreStart { // (undocumented) overlays: OverlayStart; // (undocumented) + savedObjects: SavedObjectsStart; + // (undocumented) uiSettings: UiSettingsClientContract; } @@ -329,26 +343,104 @@ export interface FatalErrorsSetup { get$: () => Rx.Observable; } +// @public (undocumented) +export type HttpBody = BodyInit | null | any; + +// @public (undocumented) +export interface HttpErrorRequest { + // (undocumented) + error: Error; + // (undocumented) + request?: Request; +} + +// @public (undocumented) +export interface HttpErrorResponse extends HttpResponse { + // Warning: (ae-forgotten-export) The symbol "HttpFetchError" needs to be exported by the entry point index.d.ts + // + // (undocumented) + error: Error | HttpFetchError; +} + +// @public (undocumented) +export interface HttpFetchOptions extends HttpRequestInit { + // (undocumented) + headers?: HttpHeadersInit; + // (undocumented) + prependBasePath?: boolean; + // (undocumented) + query?: HttpFetchQuery; +} + +// @public (undocumented) +export interface HttpFetchQuery { + // (undocumented) + [key: string]: string | number | boolean | undefined; +} + +// @public (undocumented) +export type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise; + +// @public (undocumented) +export interface HttpHeadersInit { + // (undocumented) + [name: string]: any; +} + // @public (undocumented) export interface HttpInterceptor { // Warning: (ae-forgotten-export) The symbol "HttpInterceptController" needs to be exported by the entry point index.d.ts // // (undocumented) request?(request: Request, controller: HttpInterceptController): Promise | Request | void; - // Warning: (ae-forgotten-export) The symbol "HttpErrorRequest" needs to be exported by the entry point index.d.ts - // // (undocumented) requestError?(httpErrorRequest: HttpErrorRequest, controller: HttpInterceptController): Promise | Request | void; - // Warning: (ae-forgotten-export) The symbol "HttpResponse" needs to be exported by the entry point index.d.ts - // // (undocumented) response?(httpResponse: HttpResponse, controller: HttpInterceptController): Promise | HttpResponse | void; - // Warning: (ae-forgotten-export) The symbol "HttpErrorResponse" needs to be exported by the entry point index.d.ts - // // (undocumented) responseError?(httpErrorResponse: HttpErrorResponse, controller: HttpInterceptController): Promise | HttpResponse | void; } +// @public (undocumented) +export interface HttpRequestInit { + // (undocumented) + body?: BodyInit | null; + // (undocumented) + cache?: RequestCache; + // (undocumented) + credentials?: RequestCredentials; + // (undocumented) + headers?: HttpHeadersInit; + // (undocumented) + integrity?: string; + // (undocumented) + keepalive?: boolean; + // (undocumented) + method?: string; + // (undocumented) + mode?: RequestMode; + // (undocumented) + redirect?: RequestRedirect; + // (undocumented) + referrer?: string; + // (undocumented) + referrerPolicy?: ReferrerPolicy; + // (undocumented) + signal?: AbortSignal | null; + // (undocumented) + window?: any; +} + +// @public (undocumented) +export interface HttpResponse { + // (undocumented) + body?: HttpBody; + // (undocumented) + request: Request; + // (undocumented) + response?: Response; +} + // @public (undocumented) export interface HttpServiceBase { // (undocumented) @@ -361,8 +453,6 @@ export interface HttpServiceBase { }; // (undocumented) delete: HttpHandler; - // Warning: (ae-forgotten-export) The symbol "HttpHandler" needs to be exported by the entry point index.d.ts - // // (undocumented) fetch: HttpHandler; // (undocumented) @@ -400,6 +490,19 @@ export interface I18nStart { }) => JSX.Element; } +// @public +export interface IContextContainer { + // Warning: (ae-forgotten-export) The symbol "Promisify" needs to be exported by the entry point index.d.ts + createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler): (...rest: THandlerParameters) => Promisify; + registerContext(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; +} + +// @public +export type IContextHandler = (context: TContext, ...rest: THandlerParameters) => TReturn; + +// @public +export type IContextProvider, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName]; + // @internal (undocumented) export interface InternalCoreSetup extends CoreSetup { // (undocumented) @@ -469,6 +572,7 @@ export interface OverlayStart { }) => OverlayRef; // (undocumented) openModal: (modalChildren: React.ReactNode, modalProps?: { + className?: string; closeButtonAriaLabel?: string; 'data-test-subj'?: string; }) => OverlayRef; @@ -489,8 +593,12 @@ export type PluginInitializer = T extends (...args: any[]) => any ? T : T ext [K in keyof T]: RecursiveReadonly; }> : T; +// @public (undocumented) +export interface SavedObject { + attributes: T; + // (undocumented) + error?: { + message: string; + statusCode: number; + }; + id: string; + migrationVersion?: SavedObjectsMigrationVersion; + references: SavedObjectReference[]; + type: string; + updated_at?: string; + version?: string; +} + +// @public (undocumented) +export type SavedObjectAttribute = string | number | boolean | null | undefined | SavedObjectAttributes | SavedObjectAttributes[]; + +// @public +export interface SavedObjectAttributes { + // (undocumented) + [key: string]: SavedObjectAttribute | SavedObjectAttribute[]; +} + +// @public +export interface SavedObjectReference { + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsBaseOptions { + namespace?: string; +} + +// @public (undocumented) +export interface SavedObjectsBatchResponse { + // (undocumented) + savedObjects: Array>; +} + +// @public (undocumented) +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { + // (undocumented) + attributes: T; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsBulkCreateOptions { + overwrite?: boolean; +} + +// @public +export class SavedObjectsClient { + // @internal + constructor(http: HttpServiceBase); + bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; + bulkGet: (objects?: { + id: string; + type: string; + }[]) => Promise>; + create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; + delete: (type: string, id: string) => Promise<{}>; + find: (options?: Pick) => Promise>; + get: (type: string, id: string) => Promise>; + update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; +} + +// @public +export type SavedObjectsClientContract = PublicMethodsOf; + +// @public (undocumented) +export interface SavedObjectsCreateOptions { + id?: string; + migrationVersion?: SavedObjectsMigrationVersion; + overwrite?: boolean; + // (undocumented) + references?: SavedObjectReference[]; +} + +// @public (undocumented) +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { + // (undocumented) + defaultSearchOperator?: 'AND' | 'OR'; + fields?: string[]; + // (undocumented) + hasReference?: { + type: string; + id: string; + }; + // (undocumented) + page?: number; + // (undocumented) + perPage?: number; + search?: string; + searchFields?: string[]; + // (undocumented) + sortField?: string; + // (undocumented) + sortOrder?: string; + // (undocumented) + type?: string | string[]; +} + +// @public +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { + // (undocumented) + page: number; + // (undocumented) + perPage: number; + // (undocumented) + total: number; +} + +// @public +export interface SavedObjectsMigrationVersion { + // (undocumented) + [pluginName: string]: string; +} + +// @public (undocumented) +export interface SavedObjectsStart { + // (undocumented) + client: SavedObjectsClientContract; +} + +// @public (undocumented) +export interface SavedObjectsUpdateOptions { + migrationVersion?: SavedObjectsMigrationVersion; + // (undocumented) + references?: SavedObjectReference[]; + // (undocumented) + version?: string; +} + +// @public +export class SimpleSavedObject { + constructor(client: SavedObjectsClient, { id, type, version, attributes, error, references, migrationVersion }: SavedObject); + // (undocumented) + attributes: T; + // (undocumented) + delete(): Promise<{}>; + // (undocumented) + error: SavedObject['error']; + // (undocumented) + get(key: string): any; + // (undocumented) + has(key: string): boolean; + // (undocumented) + id: SavedObject['id']; + // (undocumented) + migrationVersion: SavedObject['migrationVersion']; + // (undocumented) + references: SavedObject['references']; + // (undocumented) + save(): Promise>; + // (undocumented) + set(key: string, value: any): T; + // (undocumented) + type: SavedObject['type']; + // (undocumented) + _version?: SavedObject['version']; +} + export { Toast } // Warning: (ae-forgotten-export) The symbol "ToastInputFields" needs to be exported by the entry point index.d.ts diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts new file mode 100644 index 000000000000..f82112c7a65b --- /dev/null +++ b/src/core/public/saved_objects/index.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { + SavedObjectsBatchResponse, + SavedObjectsBulkCreateObject, + SavedObjectsBulkCreateOptions, + SavedObjectsClient, + SavedObjectsClientContract, + SavedObjectsCreateOptions, + SavedObjectsFindResponsePublic, + SavedObjectsUpdateOptions, +} from './saved_objects_client'; +export { SimpleSavedObject } from './simple_saved_object'; +export { SavedObjectsStart } from './saved_objects_service'; +export { + SavedObject, + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsBaseOptions, + SavedObjectsFindOptions, + SavedObjectsMigrationVersion, +} from '../../server/types'; diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts new file mode 100644 index 000000000000..4c0fe90a5bfb --- /dev/null +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -0,0 +1,433 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { SavedObjectsClient } from './saved_objects_client'; +import { SimpleSavedObject } from './simple_saved_object'; +import { httpServiceMock } from '../http/http_service.mock'; + +describe('SavedObjectsClient', () => { + const doc = { + id: 'AVwSwFxtcMV38qjDZoQg', + type: 'config', + attributes: { title: 'Example title' }, + version: 'foo', + }; + + const http = httpServiceMock.createStartContract(); + let savedObjectsClient: SavedObjectsClient; + + beforeEach(() => { + savedObjectsClient = new SavedObjectsClient(http); + http.fetch.mockClear(); + }); + + describe('#get', () => { + beforeEach(() => { + http.fetch.mockResolvedValue({ saved_objects: [doc] }); + }); + + test('rejects if `type` parameter is undefined', () => { + return expect( + savedObjectsClient.get(undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and id]`); + }); + + test('rejects if `id` parameter is undefined', () => { + return expect( + savedObjectsClient.get('index-pattern', undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and id]`); + }); + + test('rejects when HTTP call fails', () => { + http.fetch.mockRejectedValue(new Error('Request failed')); + return expect(savedObjectsClient.get(doc.type, doc.id)).rejects.toMatchInlineSnapshot( + `[Error: Request failed]` + ); + }); + + test('makes HTTP call', async () => { + await savedObjectsClient.get(doc.type, doc.id); + expect(http.fetch.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/saved_objects/_bulk_get", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\"}]", + "method": "POST", + "query": undefined, + }, + ] + `); + }); + + test('batches several #get calls into a single HTTP call', async () => { + // Await #get call to ensure batchQueue is empty and throttle has reset + await savedObjectsClient.get('type2', doc.id); + http.fetch.mockClear(); + + // Make two #get calls right after one another + savedObjectsClient.get('type1', doc.id); + await savedObjectsClient.get('type0', doc.id); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_get", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"type1\\"},{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"type0\\"}]", + "method": "POST", + "query": undefined, + }, + ], + ] + `); + }); + + test('resolves with SimpleSavedObject instance', async () => { + const response = savedObjectsClient.get(doc.type, doc.id); + await expect(response).resolves.toBeInstanceOf(SimpleSavedObject); + + const result = await response; + expect(result.type).toBe('config'); + expect(result.get('title')).toBe('Example title'); + }); + }); + + describe('#delete', () => { + beforeEach(() => { + http.fetch.mockResolvedValue({}); + }); + + test('rejects if `type` parameter is undefined', async () => { + expect( + savedObjectsClient.delete(undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and id]`); + }); + + test('throws if `id` parameter is undefined', async () => { + expect( + savedObjectsClient.delete('index-pattern', undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and id]`); + }); + + test('makes HTTP call', async () => { + await expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).resolves.toEqual({}); + expect(http.fetch.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/saved_objects/index-pattern/logstash-*", + Object { + "body": undefined, + "method": "DELETE", + "query": undefined, + }, + ] + `); + }); + }); + + describe('#update', () => { + const attributes = { foo: 'Foo', bar: 'Bar' }; + const options = { version: '1' }; + + beforeEach(() => { + http.fetch.mockResolvedValue({ type: 'index-pattern', attributes }); + }); + + test('rejects if `type` is undefined', async () => { + expect( + savedObjectsClient.update(undefined as any, undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type, id and attributes]`); + }); + + test('rejects if `id` is undefined', async () => { + expect( + savedObjectsClient.update('index-pattern', undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type, id and attributes]`); + }); + + test('rejects if `attributes` is undefined', async () => { + expect( + savedObjectsClient.update('index-pattern', 'logstash-*', undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type, id and attributes]`); + }); + + test('makes HTTP call', () => { + savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern/logstash-*", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"},\\"version\\":\\"1\\"}", + "method": "PUT", + "query": undefined, + }, + ], + ] + `); + }); + + test('rejects when HTTP call fails', async () => { + http.fetch.mockRejectedValueOnce(new Error('Request failed')); + await expect( + savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options) + ).rejects.toMatchInlineSnapshot(`[Error: Request failed]`); + }); + + test('resolves with SimpleSavedObject instance', async () => { + const response = savedObjectsClient.update( + 'index-pattern', + 'logstash-*', + attributes, + options + ); + await expect(response).resolves.toBeInstanceOf(SimpleSavedObject); + + const result = await response; + expect(result.type).toBe('index-pattern'); + expect(result.get('foo')).toBe('Foo'); + }); + }); + + describe('#create', () => { + const attributes = { foo: 'Foo', bar: 'Bar' }; + + beforeEach(() => { + http.fetch.mockResolvedValue({ id: 'serverId', type: 'server-type', attributes }); + }); + + test('rejects if `type` is undefined', async () => { + await expect( + savedObjectsClient.create(undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and attributes]`); + }); + + test('resolves with SimpleSavedObject instance', async () => { + const response = savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }); + await expect(response).resolves.toBeInstanceOf(SimpleSavedObject); + + const result = await response; + + expect(result.type).toBe('server-type'); + expect(result.id).toBe('serverId'); + expect(result.attributes).toBe(attributes); + }); + + test('makes HTTP call with ID', () => { + savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern/myId", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"}}", + "method": "POST", + "query": Object { + "overwrite": undefined, + }, + }, + ], + ] + `); + }); + + test('makes HTTP call without ID', () => { + savedObjectsClient.create('index-pattern', attributes); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"}}", + "method": "POST", + "query": Object { + "overwrite": undefined, + }, + }, + ], + ] + `); + }); + + test('rejects when HTTP call fails', async () => { + http.fetch.mockRejectedValueOnce(new Error('Request failed')); + await expect( + savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }) + ).rejects.toMatchInlineSnapshot(`[Error: Request failed]`); + }); + }); + + describe('#bulk_create', () => { + beforeEach(() => { + http.fetch.mockResolvedValue({ saved_objects: [doc] }); + }); + + test('resolves with array of SimpleSavedObject instances', async () => { + const response = savedObjectsClient.bulkCreate([doc]); + await expect(response).resolves.toHaveProperty('savedObjects'); + + const result = await response; + expect(result.savedObjects).toHaveLength(1); + expect(result.savedObjects[0]).toBeInstanceOf(SimpleSavedObject); + }); + + test('makes HTTP call', async () => { + await savedObjectsClient.bulkCreate([doc]); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_create", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\"}]", + "method": "POST", + "query": Object { + "overwrite": false, + }, + }, + ], + ] + `); + }); + + test('makes HTTP call with overwrite query paramater', async () => { + await savedObjectsClient.bulkCreate([doc], { overwrite: true }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_create", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\"}]", + "method": "POST", + "query": Object { + "overwrite": true, + }, + }, + ], + ] + `); + }); + }); + + describe('#find', () => { + const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' }; + + beforeEach(() => { + http.fetch.mockResolvedValue({ saved_objects: [object], page: 0, per_page: 1, total: 1 }); + }); + + test('resolves with instances of SimpleSavedObjects', async () => { + const options = { type: 'index-pattern' }; + const resultP = savedObjectsClient.find(options); + await expect(resultP).resolves.toHaveProperty('savedObjects'); + + const result = await resultP; + expect(result.savedObjects).toHaveLength(1); + expect(result.savedObjects[0]).toBeInstanceOf(SimpleSavedObject); + expect(result.page).toBe(0); + expect(result.perPage).toBe(1); + expect(result.total).toBe(1); + }); + + test('makes HTTP call correctly mapping options into snake case query parameters', () => { + const options = { + defaultSearchOperator: 'OR' as const, + fields: ['title'], + hasReference: { id: '1', type: 'reference' }, + page: 10, + perPage: 100, + search: 'what is the meaning of life?|life', + searchFields: ['title^5', 'body'], + sortField: 'sort_field', + type: 'index-pattern', + }; + + savedObjectsClient.find(options); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_find", + Object { + "body": undefined, + "method": "GET", + "query": Object { + "default_search_operator": "OR", + "fields": Array [ + "title", + ], + "has_reference": Object { + "id": "1", + "type": "reference", + }, + "page": 10, + "per_page": 100, + "search": "what is the meaning of life?|life", + "search_fields": Array [ + "title^5", + "body", + ], + "sort_field": "sort_field", + "type": "index-pattern", + }, + }, + ], + ] + `); + }); + + test('ignores invalid options', () => { + const options = { + invalid: true, + namespace: 'default', + sortOrder: 'sort', // Not currently supported by API + }; + + // @ts-ignore + savedObjectsClient.find(options); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_find", + Object { + "body": undefined, + "method": "GET", + "query": Object {}, + }, + ], + ] + `); + }); + }); + + it('maintains backwards compatibility by transforming http.fetch errors to be compatible with kfetch errors', () => { + const err = { + response: { ok: false, redirected: false, status: 409, statusText: 'Conflict' }, + body: 'response body', + }; + http.fetch.mockRejectedValue(err); + return expect(savedObjectsClient.get(doc.type, doc.id)).rejects.toMatchInlineSnapshot(` + Object { + "body": "response body", + "res": Object { + "ok": false, + "redirected": false, + "status": 409, + "statusText": "Conflict", + }, + } + `); + }); +}); diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts new file mode 100644 index 000000000000..b0768826159c --- /dev/null +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -0,0 +1,453 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, pick, throttle } from 'lodash'; +import { resolve as resolveUrl } from 'url'; + +import { + SavedObject, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsClientContract as SavedObjectsApi, + SavedObjectsFindOptions as SavedObjectFindOptionsServer, + SavedObjectsMigrationVersion, +} from '../../server'; + +// TODO: Migrate to an error modal powered by the NP? +import { + isAutoCreateIndexError, + showAutoCreateIndexErrorPage, +} from '../../../legacy/ui/public/error_auto_create_index/error_auto_create_index'; +import { SimpleSavedObject } from './simple_saved_object'; +import { HttpFetchOptions, HttpServiceBase } from '../http'; + +type SavedObjectsFindOptions = Omit; + +type PromiseType> = T extends Promise ? U : never; + +/** @public */ +export interface SavedObjectsCreateOptions { + /** + * (Not recommended) Specify an id instead of having the saved objects service generate one for you. + */ + id?: string; + /** If a document with the given `id` already exists, overwrite it's contents (default=false). */ + overwrite?: boolean; + /** {@inheritDoc SavedObjectsMigrationVersion} */ + migrationVersion?: SavedObjectsMigrationVersion; + references?: SavedObjectReference[]; +} + +/** + * @param type - Create a SavedObject of the given type + * @param attributes - Create a SavedObject with the given attributes + * + * @public + */ +export interface SavedObjectsBulkCreateObject< + T extends SavedObjectAttributes = SavedObjectAttributes +> extends SavedObjectsCreateOptions { + type: string; + attributes: T; +} + +/** @public */ +export interface SavedObjectsBulkCreateOptions { + /** If a document with the given `id` already exists, overwrite it's contents (default=false). */ + overwrite?: boolean; +} + +/** @public */ +export interface SavedObjectsUpdateOptions { + version?: string; + /** {@inheritDoc SavedObjectsMigrationVersion} */ + migrationVersion?: SavedObjectsMigrationVersion; + references?: SavedObjectReference[]; +} + +/** @public */ +export interface SavedObjectsBatchResponse< + T extends SavedObjectAttributes = SavedObjectAttributes +> { + savedObjects: Array>; +} + +/** + * Return type of the Saved Objects `find()` method. + * + * *Note*: this type is different between the Public and Server Saved Objects + * clients. + * + * @public + */ +export interface SavedObjectsFindResponsePublic< + T extends SavedObjectAttributes = SavedObjectAttributes +> extends SavedObjectsBatchResponse { + total: number; + perPage: number; + page: number; +} + +interface BatchQueueEntry { + type: string; + id: string; + resolve: (value: SimpleSavedObject | SavedObject) => void; + reject: (reason?: any) => void; +} + +const join = (...uriComponents: Array) => + uriComponents + .filter((comp): comp is string => Boolean(comp)) + .map(encodeURIComponent) + .join('/'); + +/** + * Interval that requests are batched for + * @type {integer} + */ +const BATCH_INTERVAL = 100; + +const API_BASE_URL = '/api/saved_objects/'; + +/** + * SavedObjectsClientContract as implemented by the {@link SavedObjectsClient} + * + * @public + */ +export type SavedObjectsClientContract = PublicMethodsOf; + +/** + * Saved Objects is Kibana's data persisentence mechanism allowing plugins to + * use Elasticsearch for storing plugin state. The client-side + * SavedObjectsClient is a thin convenience library around the SavedObjects + * HTTP API for interacting with Saved Objects. + * + * @public + */ +export class SavedObjectsClient { + private http: HttpServiceBase; + private batchQueue: BatchQueueEntry[]; + + /** + * Throttled processing of get requests into bulk requests at 100ms interval + */ + private processBatchQueue = throttle( + () => { + const queue = cloneDeep(this.batchQueue); + this.batchQueue = []; + + this.bulkGet(queue) + .then(({ savedObjects }) => { + queue.forEach(queueItem => { + const foundObject = savedObjects.find(savedObject => { + return savedObject.id === queueItem.id && savedObject.type === queueItem.type; + }); + + if (!foundObject) { + return queueItem.resolve(this.createSavedObject(pick(queueItem, ['id', 'type']))); + } + + queueItem.resolve(foundObject); + }); + }) + .catch(err => { + queue.forEach(queueItem => { + queueItem.reject(err); + }); + }); + }, + BATCH_INTERVAL, + { leading: false } + ); + + /** @internal */ + constructor(http: HttpServiceBase) { + this.http = http; + this.batchQueue = []; + } + + /** + * Persists an object + * + * @param type + * @param attributes + * @param options + * @returns + */ + public create = ( + type: string, + attributes: T, + options: SavedObjectsCreateOptions = {} + ): Promise> => { + if (!type || !attributes) { + return Promise.reject(new Error('requires type and attributes')); + } + + const path = this.getPath([type, options.id]); + const query = { + overwrite: options.overwrite, + }; + + const createRequest: Promise> = this.savedObjectsFetch(path, { + method: 'POST', + query, + body: JSON.stringify({ + attributes, + migrationVersion: options.migrationVersion, + references: options.references, + }), + }); + + return createRequest + .then(resp => this.createSavedObject(resp)) + .catch((error: object) => { + if (isAutoCreateIndexError(error)) { + showAutoCreateIndexErrorPage(); + } + + throw error; + }); + }; + + /** + * Creates multiple documents at once + * + * @param {array} objects - [{ type, id, attributes, references, migrationVersion }] + * @param {object} [options={}] + * @property {boolean} [options.overwrite=false] + * @returns The result of the create operation containing created saved objects. + */ + public bulkCreate = ( + objects: SavedObjectsBulkCreateObject[] = [], + options: SavedObjectsBulkCreateOptions = { overwrite: false } + ) => { + const path = this.getPath(['_bulk_create']); + const query = { overwrite: options.overwrite }; + + const request: ReturnType = this.savedObjectsFetch(path, { + method: 'POST', + query, + body: JSON.stringify(objects), + }); + return request.then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); + return renameKeys< + PromiseType>, + SavedObjectsBatchResponse + >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse; + }); + }; + + /** + * Deletes an object + * + * @param type + * @param id + * @returns + */ + public delete = (type: string, id: string): ReturnType => { + if (!type || !id) { + return Promise.reject(new Error('requires type and id')); + } + + return this.savedObjectsFetch(this.getPath([type, id]), { method: 'DELETE' }); + }; + + /** + * Search for objects + * + * @param {object} [options={}] + * @property {string} options.type + * @property {string} options.search + * @property {string} options.searchFields - see Elasticsearch Simple Query String + * Query field argument for more information + * @property {integer} [options.page=1] + * @property {integer} [options.perPage=20] + * @property {array} options.fields + * @property {object} [options.hasReference] - { type, id } + * @returns A find result with objects matching the specified search. + */ + public find = ( + options: SavedObjectsFindOptions = {} + ): Promise> => { + const path = this.getPath(['_find']); + const renameMap = { + defaultSearchOperator: 'default_search_operator', + fields: 'fields', + hasReference: 'has_reference', + page: 'page', + perPage: 'per_page', + search: 'search', + searchFields: 'search_fields', + sortField: 'sort_field', + type: 'type', + }; + + const renamedQuery = renameKeys(renameMap, options); + const query = pick.apply(null, [renamedQuery, ...Object.values(renameMap)]); + + const request: ReturnType = this.savedObjectsFetch(path, { + method: 'GET', + query, + }); + return request.then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); + return renameKeys< + PromiseType>, + SavedObjectsFindResponsePublic + >( + { + saved_objects: 'savedObjects', + total: 'total', + per_page: 'perPage', + page: 'page', + }, + resp + ) as SavedObjectsFindResponsePublic; + }); + }; + + /** + * Fetches a single object + * + * @param {string} type + * @param {string} id + * @returns The saved object for the given type and id. + */ + public get = ( + type: string, + id: string + ): Promise> => { + if (!type || !id) { + return Promise.reject(new Error('requires type and id')); + } + + return new Promise((resolve, reject) => { + this.batchQueue.push({ type, id, resolve, reject } as BatchQueueEntry); + this.processBatchQueue(); + }); + }; + + /** + * Returns an array of objects by id + * + * @param {array} objects - an array ids, or an array of objects containing id and optionally type + * @returns The saved objects with the given type and ids requested + * @example + * + * bulkGet([ + * { id: 'one', type: 'config' }, + * { id: 'foo', type: 'index-pattern' } + * ]) + */ + public bulkGet = (objects: Array<{ id: string; type: string }> = []) => { + const path = this.getPath(['_bulk_get']); + const filteredObjects = objects.map(obj => pick(obj, ['id', 'type'])); + + const request: ReturnType = this.savedObjectsFetch(path, { + method: 'POST', + body: JSON.stringify(filteredObjects), + }); + return request.then(resp => { + resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); + return renameKeys< + PromiseType>, + SavedObjectsBatchResponse + >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse; + }); + }; + + /** + * Updates an object + * + * @param {string} type + * @param {string} id + * @param {object} attributes + * @param {object} options + * @prop {integer} options.version - ensures version matches that of persisted object + * @prop {object} options.migrationVersion - The optional migrationVersion of this document + * @returns + */ + public update( + type: string, + id: string, + attributes: T, + { version, migrationVersion, references }: SavedObjectsUpdateOptions = {} + ): Promise> { + if (!type || !id || !attributes) { + return Promise.reject(new Error('requires type, id and attributes')); + } + + const path = this.getPath([type, id]); + const body = { + attributes, + migrationVersion, + references, + version, + }; + + return this.savedObjectsFetch(path, { + method: 'PUT', + body: JSON.stringify(body), + }).then((resp: SavedObject) => { + return this.createSavedObject(resp); + }); + } + + private createSavedObject( + options: SavedObject + ): SimpleSavedObject { + return new SimpleSavedObject(this, options); + } + + private getPath(path: Array): string { + return resolveUrl(API_BASE_URL, join(...path)); + } + + /** + * To ensure we don't break backwards compatibility, savedObjectsFetch keeps + * the old kfetch error format of `{res: {status: number}}` whereas `http.fetch` + * uses `{response: {status: number}}`. + */ + private savedObjectsFetch(path: string, { method, query, body }: HttpFetchOptions) { + return this.http.fetch(path, { method, query, body }).catch(err => { + const kfetchError = Object.assign(err, { res: err.response }); + delete kfetchError.response; + return Promise.reject(kfetchError); + }); + } +} + +/** + * Returns a new object with the own properties of `obj`, but the + * keys renamed according to the `keysMap`. + * + * @param keysMap - a map of the form `{oldKey: newKey}` + * @param obj - the object whose own properties will be renamed + */ +const renameKeys = , U extends Record>( + keysMap: Record, + obj: Record +) => + Object.keys(obj).reduce((acc, key) => { + return { + ...acc, + ...{ [keysMap[key] || key]: obj[key] }, + }; + }, {}); diff --git a/src/core/public/saved_objects/saved_objects_service.mock.ts b/src/core/public/saved_objects/saved_objects_service.mock.ts new file mode 100644 index 000000000000..feace09806a9 --- /dev/null +++ b/src/core/public/saved_objects/saved_objects_service.mock.ts @@ -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 { SavedObjectsService, SavedObjectsStart } from './saved_objects_service'; + +const createStartContractMock = () => { + const mock: jest.Mocked = { + client: { + create: jest.fn(), + bulkCreate: jest.fn(), + delete: jest.fn(), + bulkGet: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }, + }; + return mock; +}; + +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + mocked.start.mockReturnValue(Promise.resolve(createStartContractMock())); + return mocked; +}; + +export const savedObjectsMock = { + create: createMock, + createStartContract: createStartContractMock, +}; diff --git a/src/core/public/saved_objects/saved_objects_service.ts b/src/core/public/saved_objects/saved_objects_service.ts new file mode 100644 index 000000000000..bb91c6f340c0 --- /dev/null +++ b/src/core/public/saved_objects/saved_objects_service.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { CoreService } from 'src/core/types'; +import { CoreStart } from 'src/core/public'; +import { SavedObjectsClient, SavedObjectsClientContract } from './saved_objects_client'; + +/** + * @public + */ +export interface SavedObjectsStart { + /** {@link SavedObjectsClient} */ + client: SavedObjectsClientContract; +} + +export class SavedObjectsService implements CoreService { + public async setup() {} + public async start({ http }: { http: CoreStart['http'] }): Promise { + return { client: new SavedObjectsClient(http) }; + } + public async stop() {} +} diff --git a/src/legacy/ui/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts similarity index 90% rename from src/legacy/ui/public/saved_objects/simple_saved_object.ts rename to src/core/public/saved_objects/simple_saved_object.ts index 26488bdeb1ab..92c228edd5e8 100644 --- a/src/legacy/ui/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -18,16 +18,17 @@ */ import { get, has, set } from 'lodash'; -import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/server'; +import { SavedObject as SavedObjectType, SavedObjectAttributes } from '../../server'; import { SavedObjectsClient } from './saved_objects_client'; /** - * This class is a very simple wrapper for SavedObjects loaded from the server. + * This class is a very simple wrapper for SavedObjects loaded from the server + * with the {@link SavedObjectsClient}. * - * It provides basic functionality for updating/deleting/etc. saved objects but - * doesn't include any type-specific implementations. + * It provides basic functionality for creating/saving/deleting saved objects, + * but doesn't include any type-specific implementations. * - * For more sophisiticated use cases, the SavedObject class implements additional functions + * @public */ export class SimpleSavedObject { public attributes: T; diff --git a/src/core/server/config/config.ts b/src/core/server/config/config.ts index f054817fe9ee..b1a6a8cc525b 100644 --- a/src/core/server/config/config.ts +++ b/src/core/server/config/config.ts @@ -17,6 +17,7 @@ * under the License. */ +/** @public */ export type ConfigPath = string | string[]; /** diff --git a/src/core/server/context/context_service.mock.ts b/src/core/server/context/context_service.mock.ts new file mode 100644 index 000000000000..eb55ced69dc0 --- /dev/null +++ b/src/core/server/context/context_service.mock.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ContextService, ContextSetup } from './context_service'; +import { contextMock } from '../../utils/context.mock'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + createContextContainer: jest.fn().mockImplementation(() => contextMock.create()), + }; + return setupContract; +}; + +type ContextServiceContract = PublicMethodsOf; +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + }; + mocked.setup.mockReturnValue(createSetupContractMock()); + return mocked; +}; + +export const contextServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/server/context/context_service.test.mocks.ts b/src/core/server/context/context_service.test.mocks.ts new file mode 100644 index 000000000000..5cf492d97aaf --- /dev/null +++ b/src/core/server/context/context_service.test.mocks.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 { contextMock } from '../../utils/context.mock'; + +export const MockContextConstructor = jest.fn(contextMock.create); +jest.doMock('../../utils/context', () => ({ + ContextContainer: MockContextConstructor, +})); diff --git a/src/core/server/context/context_service.test.ts b/src/core/server/context/context_service.test.ts new file mode 100644 index 000000000000..611ba8cac92a --- /dev/null +++ b/src/core/server/context/context_service.test.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 { PluginOpaqueId } from '../../server'; +import { MockContextConstructor } from './context_service.test.mocks'; +import { ContextService } from './context_service'; +import { CoreContext } from '../core_context'; + +const pluginDependencies = new Map(); + +describe('ContextService', () => { + describe('#setup()', () => { + test('createContextContainer returns a new container configured with pluginDependencies', () => { + const coreId = Symbol(); + const service = new ContextService({ coreId } as CoreContext); + const setup = service.setup({ pluginDependencies }); + expect(setup.createContextContainer()).toBeDefined(); + expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, coreId); + }); + }); +}); diff --git a/src/core/server/context/context_service.ts b/src/core/server/context/context_service.ts new file mode 100644 index 000000000000..80935840c553 --- /dev/null +++ b/src/core/server/context/context_service.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { PluginOpaqueId } from '../../server'; +import { IContextContainer, ContextContainer } from '../../utils/context'; +import { CoreContext } from '../core_context'; + +interface SetupDeps { + pluginDependencies: ReadonlyMap; +} + +/** @internal */ +export class ContextService { + constructor(private readonly core: CoreContext) {} + + public setup({ pluginDependencies }: SetupDeps): ContextSetup { + return { + createContextContainer: < + TContext extends {}, + THandlerReturn, + THandlerParameters extends any[] = [] + >() => { + return new ContextContainer( + pluginDependencies, + this.core.coreId + ); + }, + }; + } +} + +/** + * {@inheritdoc IContextContainer} + * + * @example + * Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we + * want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. + * ```ts + * export interface VizRenderContext { + * core: { + * i18n: I18nStart; + * uiSettings: UISettingsClientContract; + * } + * [contextName: string]: unknown; + * } + * + * export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; + * + * class VizRenderingPlugin { + * private readonly vizRenderers = new Map () => void)>(); + * + * constructor(private readonly initContext: PluginInitializerContext) {} + * + * setup(core) { + * this.contextContainer = core.context.createContextContainer< + * VizRenderContext, + * ReturnType, + * [HTMLElement] + * >(); + * + * return { + * registerContext: this.contextContainer.registerContext, + * registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) => + * this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)), + * }; + * } + * + * start(core) { + * // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. + * this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ + * i18n: core.i18n, + * uiSettings: core.uiSettings + * })); + * + * return { + * registerContext: this.contextContainer.registerContext, + * + * renderVizualization: (renderMethod: string, domElement: HTMLElement) => { + * if (!this.vizRenderer.has(renderMethod)) { + * throw new Error(`Render method '${renderMethod}' has not been registered`); + * } + * + * // The handler can now be called directly with only an `HTMLElement` and will automatically + * // have a new `context` object created and populated by the context container. + * const handler = this.vizRenderers.get(renderMethod) + * return handler(domElement); + * } + * }; + * } + * } + * ``` + * + * @public + */ +export interface ContextSetup { + /** + * Creates a new {@link IContextContainer} for a service owner. + */ + createContextContainer< + TContext extends {}, + THandlerReturn, + THandlerParmaters extends any[] = [] + >(): IContextContainer; +} diff --git a/src/core/server/context/index.ts b/src/core/server/context/index.ts new file mode 100644 index 000000000000..28b2641b2a5a --- /dev/null +++ b/src/core/server/context/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 { ContextService, ContextSetup } from './context_service'; +export { IContextContainer, IContextProvider, IContextHandler } from '../../utils/context'; diff --git a/src/core/server/core_context.ts b/src/core/server/core_context.ts index a08bd03f148c..701f5a83a81c 100644 --- a/src/core/server/core_context.ts +++ b/src/core/server/core_context.ts @@ -20,12 +20,16 @@ import { ConfigService, Env } from './config'; import { LoggerFactory } from './logging'; +/** @internal */ +export type CoreId = symbol; + /** * Groups all main Kibana's core modules/systems/services that are consumed in a * variety of places within the core itself. * @internal */ export interface CoreContext { + coreId: CoreId; env: Env; configService: ConfigService; logger: LoggerFactory; diff --git a/src/core/server/elasticsearch/cluster_client.ts b/src/core/server/elasticsearch/cluster_client.ts index c8500499d970..aa40010bf3b1 100644 --- a/src/core/server/elasticsearch/cluster_client.ts +++ b/src/core/server/elasticsearch/cluster_client.ts @@ -18,10 +18,9 @@ */ import { Client } from 'elasticsearch'; import { get } from 'lodash'; -import { Request } from 'hapi'; import { ElasticsearchErrorHelpers } from './errors'; -import { GetAuthHeaders, isRealRequest } from '../http'; +import { GetAuthHeaders, isRealRequest, LegacyRequest } from '../http'; import { filterHeaders, Headers, KibanaRequest, ensureRawRequest } from '../http/router'; import { Logger } from '../logging'; import { @@ -36,8 +35,6 @@ import { ScopedClusterClient } from './scoped_cluster_client'; * @public */ -export type LegacyRequest = Request; - const noop = () => undefined; /** * The set of options that defines how API call should be made and result be diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index e84ddb2baa30..58fe2b52727e 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -54,7 +54,7 @@ const logger = loggingServiceMock.create(); beforeEach(() => { env = Env.createDefault(getEnvOptions()); - coreContext = { env, logger, configService: configService as any }; + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; elasticsearchService = new ElasticsearchService(coreContext); }); diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 1d439dfba49e..f732f9e39b9e 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -18,7 +18,7 @@ */ export { ElasticsearchServiceSetup, ElasticsearchService } from './elasticsearch_service'; -export { CallAPIOptions, ClusterClient, FakeRequest, LegacyRequest } from './cluster_client'; +export { CallAPIOptions, ClusterClient, FakeRequest } from './cluster_client'; export { ScopedClusterClient, Headers, APICaller } from './scoped_cluster_client'; export { ElasticsearchClientConfig } from './elasticsearch_client_config'; export { config } from './elasticsearch_config'; diff --git a/src/core/server/http/auth_headers_storage.ts b/src/core/server/http/auth_headers_storage.ts index bc3b55b3718c..469e194a61fe 100644 --- a/src/core/server/http/auth_headers_storage.ts +++ b/src/core/server/http/auth_headers_storage.ts @@ -16,19 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from 'hapi'; -import { KibanaRequest, ensureRawRequest } from './router'; +import { KibanaRequest, ensureRawRequest, LegacyRequest } from './router'; import { AuthHeaders } from './lifecycle/auth'; /** * Get headers to authenticate a user against Elasticsearch. + * @param request {@link KibanaRequest} - an incoming request. + * @return authentication headers {@link AuthHeaders} for - an incoming request. * @public * */ -export type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; +export type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined; +/** @internal */ export class AuthHeadersStorage { - private authHeadersCache = new WeakMap(); - public set = (request: KibanaRequest | Request, headers: AuthHeaders) => { + private authHeadersCache = new WeakMap(); + public set = (request: KibanaRequest | LegacyRequest, headers: AuthHeaders) => { this.authHeadersCache.set(ensureRawRequest(request), headers); }; public get: GetAuthHeaders = request => { diff --git a/src/core/server/http/auth_state_storage.ts b/src/core/server/http/auth_state_storage.ts index 79fd9ed64f3b..059dc7f38035 100644 --- a/src/core/server/http/auth_state_storage.ts +++ b/src/core/server/http/auth_state_storage.ts @@ -16,22 +16,51 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from 'hapi'; -import { KibanaRequest, ensureRawRequest } from './router'; +import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; +/** + * Status indicating an outcome of the authentication. + * @public + */ export enum AuthStatus { + /** + * `auth` interceptor successfully authenticated a user + */ authenticated = 'authenticated', + /** + * `auth` interceptor failed user authentication + */ unauthenticated = 'unauthenticated', + /** + * `auth` interceptor has not been registered + */ unknown = 'unknown', } +/** + * Get authentication state for a request. Returned by `auth` interceptor. + * @param request {@link KibanaRequest} - an incoming request. + * @public + */ +export type GetAuthState = ( + request: KibanaRequest | LegacyRequest +) => { status: AuthStatus; state: unknown }; + +/** + * Return authentication status for a request. + * @param request {@link KibanaRequest} - an incoming request. + * @public + */ +export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; + +/** @internal */ export class AuthStateStorage { - private readonly storage = new WeakMap(); + private readonly storage = new WeakMap(); constructor(private readonly canBeAuthenticated: () => boolean) {} - public set = (request: KibanaRequest | Request, state: unknown) => { + public set = (request: KibanaRequest | LegacyRequest, state: unknown) => { this.storage.set(ensureRawRequest(request), state); }; - public get = (request: KibanaRequest | Request) => { + public get: GetAuthState = request => { const key = ensureRawRequest(request); const state = this.storage.get(key); const status: AuthStatus = this.storage.has(key) @@ -42,7 +71,7 @@ export class AuthStateStorage { return { status, state }; }; - public isAuthenticated = (request: KibanaRequest | Request) => { + public isAuthenticated: IsAuthenticated = request => { return this.get(request).status === AuthStatus.authenticated; }; } diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index 2335f35ab63c..ff7fee0198f6 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -18,7 +18,8 @@ */ import { ByteSizeValue } from '@kbn/config-schema'; -import { Server } from 'hapi'; +import { Server, Request } from 'hapi'; +import Url from 'url'; import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https'; import { sample } from 'lodash'; import { DevConfig } from '../dev'; @@ -146,6 +147,38 @@ export class BasePathProxyServer { path: `${this.httpConfig.basePath}/{kbnPath*}`, }); + this.server.route({ + handler: { + proxy: { + agent: this.httpsAgent, + passThrough: true, + xforward: true, + mapUri: (request: Request) => ({ + uri: Url.format({ + hostname: request.server.info.host, + port: this.devConfig.basePathProxyTargetPort, + protocol: request.server.info.protocol, + pathname: `${this.httpConfig.basePath}/${request.params.kbnPath}`, + query: request.query, + }), + headers: request.headers, + }), + }, + }, + method: '*', + options: { + pre: [ + // Before we proxy request to a target port we may want to wait until some + // condition is met (e.g. until target listener is ready). + async (request, responseToolkit) => { + await blockUntil(); + return responseToolkit.continue; + }, + ], + }, + path: `/__UNSAFE_bypassBasePath/{kbnPath*}`, + }); + // It may happen that basepath has changed, but user still uses the old one, // so we can try to check if that's the case and just redirect user to the // same URL, but with valid basepath. diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts index df189d29f2f5..951463a2c991 100644 --- a/src/core/server/http/base_path_service.ts +++ b/src/core/server/http/base_path_service.ts @@ -16,24 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from 'hapi'; -import { KibanaRequest, ensureRawRequest } from './router'; +import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; import { modifyUrl } from '../../utils'; export class BasePath { - private readonly basePathCache = new WeakMap(); + private readonly basePathCache = new WeakMap(); constructor(private readonly serverBasePath?: string) {} - public get = (request: KibanaRequest | Request) => { + public get = (request: KibanaRequest | LegacyRequest) => { const requestScopePath = this.basePathCache.get(ensureRawRequest(request)) || ''; const serverBasePath = this.serverBasePath || ''; return `${serverBasePath}${requestScopePath}`; }; // should work only for KibanaRequest as soon as spaces migrate to NP - public set = (request: KibanaRequest | Request, requestSpecificBasePath: string) => { + public set = (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => { const rawRequest = ensureRawRequest(request); if (this.basePathCache.has(rawRequest)) { diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index 7b2569a1c6dd..8a1b56d87fb4 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -24,10 +24,26 @@ import { KibanaRequest, ensureRawRequest } from './router'; import { SessionStorageFactory, SessionStorage } from './session_storage'; import { Logger } from '..'; +/** + * Configuration used to create HTTP session storage based on top of cookie mechanism. + * @public + */ export interface SessionStorageCookieOptions { + /** + * Name of the session cookie. + */ name: string; + /** + * A key used to encrypt a cookie value. Should be at least 32 characters long. + */ encryptionKey: string; + /** + * Function called to validate a cookie content. + */ validate: (sessionValue: T) => boolean | Promise; + /** + * Flag indicating whether the cookie should be sent only via a secure connection. + */ isSecure: boolean; } diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index bb52fa1fa38d..d676a67b734d 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -16,14 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -import { Request, ResponseToolkit } from 'hapi'; +import { Request } from 'hapi'; import { merge } from 'lodash'; import querystring from 'querystring'; import { schema } from '@kbn/config-schema'; -import { KibanaRequest, RouteMethod } from './router'; +import { + KibanaRequest, + LifecycleResponseFactory, + RouteMethod, + KibanaResponseFactory, +} from './router'; interface RequestFixtureOptions { headers?: Record; @@ -97,12 +102,35 @@ function createRawRequestMock(customization: DeepPartial = {}) { ) as Request; } -function createRawResponseToolkitMock(customization: DeepPartial = {}) { - return merge({}, customization) as ResponseToolkit; -} +const createResponseFactoryMock = (): jest.Mocked => ({ + ok: jest.fn(), + accepted: jest.fn(), + noContent: jest.fn(), + custom: jest.fn(), + redirected: jest.fn(), + badRequest: jest.fn(), + unauthorized: jest.fn(), + forbidden: jest.fn(), + notFound: jest.fn(), + conflict: jest.fn(), + internalError: jest.fn(), + customError: jest.fn(), +}); + +const createLifecycleResponseFactoryMock = (): jest.Mocked => ({ + redirected: jest.fn(), + badRequest: jest.fn(), + unauthorized: jest.fn(), + forbidden: jest.fn(), + notFound: jest.fn(), + conflict: jest.fn(), + internalError: jest.fn(), + customError: jest.fn(), +}); export const httpServerMock = { createKibanaRequest: createKibanaRequestMock, createRawRequest: createRawRequestMock, - createRawResponseToolkit: createRawResponseToolkitMock, + createResponseFactory: createResponseFactoryMock, + createLifecycleResponseFactory: createLifecycleResponseFactoryMock, }; diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index aa341db20a6c..f82c42317edb 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -18,8 +18,6 @@ */ import { Server } from 'http'; -import request from 'request'; -import Boom from 'boom'; jest.mock('fs', () => ({ readFileSync: jest.fn(), @@ -28,7 +26,7 @@ jest.mock('fs', () => ({ import Chance from 'chance'; import supertest from 'supertest'; -import { ByteSizeValue } from '@kbn/config-schema'; +import { ByteSizeValue, schema } from '@kbn/config-schema'; import { HttpConfig, Router } from '.'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { HttpServer } from './http_server'; @@ -40,16 +38,6 @@ const cookieOptions = { isSecure: false, }; -interface User { - id: string; - roles?: string[]; -} - -interface StorageData { - value: User; - expires: number; -} - const chance = new Chance(); let server: HttpServer; @@ -102,98 +90,17 @@ Array [ `); }); -test('200 OK with body', async () => { - const router = new Router('/foo'); - - router.get({ path: '/', validate: false }, (req, res) => { - return res.ok({ key: 'value' }); - }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - await server.start(); - - await supertest(innerServer.listener) - .get('/foo/') - .expect(200) - .then(res => { - expect(res.body).toEqual({ key: 'value' }); - }); -}); - -test('202 Accepted with body', async () => { - const router = new Router('/foo'); - - router.get({ path: '/', validate: false }, (req, res) => { - return res.accepted({ location: 'somewhere' }); - }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await supertest(innerServer.listener) - .get('/foo/') - .expect(202) - .then(res => { - expect(res.body).toEqual({ location: 'somewhere' }); - }); -}); - -test('204 No content', async () => { - const router = new Router('/foo'); - - router.get({ path: '/', validate: false }, (req, res) => { - return res.noContent(); - }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await supertest(innerServer.listener) - .get('/foo/') - .expect(204) - .then(res => { - expect(res.body).toEqual({}); - // TODO Is ^ wrong or just a result of supertest, I expect `null` or `undefined` - }); -}); - -test('400 Bad request with error', async () => { - const router = new Router('/foo'); - - router.get({ path: '/', validate: false }, (req, res) => { - const err = new Error('some message'); - return res.badRequest(err); - }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - - await server.start(); - - await supertest(innerServer.listener) - .get('/foo/') - .expect(400) - .then(res => { - expect(res.body).toEqual({ error: 'some message' }); - }); -}); - test('valid params', async () => { const router = new Router('/foo'); router.get( { path: '/{test}', - validate: schema => ({ + validate: { params: schema.object({ test: schema.string(), }), - }), + }, }, (req, res) => { return res.ok({ key: req.params.test }); @@ -219,11 +126,11 @@ test('invalid params', async () => { router.get( { path: '/{test}', - validate: schema => ({ + validate: { params: schema.object({ test: schema.number(), }), - }), + }, }, (req, res) => { return res.ok({ key: req.params.test }); @@ -240,7 +147,9 @@ test('invalid params', async () => { .expect(400) .then(res => { expect(res.body).toEqual({ - error: '[test]: expected value of type [number] but got [string]', + error: 'Bad Request', + statusCode: 400, + message: '[request params.test]: expected value of type [number] but got [string]', }); }); }); @@ -251,12 +160,12 @@ test('valid query', async () => { router.get( { path: '/', - validate: schema => ({ + validate: { query: schema.object({ bar: schema.string(), quux: schema.number(), }), - }), + }, }, (req, res) => { return res.ok(req.query); @@ -282,11 +191,11 @@ test('invalid query', async () => { router.get( { path: '/', - validate: schema => ({ + validate: { query: schema.object({ bar: schema.number(), }), - }), + }, }, (req, res) => { return res.ok(req.query); @@ -303,7 +212,9 @@ test('invalid query', async () => { .expect(400) .then(res => { expect(res.body).toEqual({ - error: '[bar]: expected value of type [number] but got [string]', + error: 'Bad Request', + statusCode: 400, + message: '[request query.bar]: expected value of type [number] but got [string]', }); }); }); @@ -314,12 +225,12 @@ test('valid body', async () => { router.post( { path: '/', - validate: schema => ({ + validate: { body: schema.object({ bar: schema.string(), baz: schema.number(), }), - }), + }, }, (req, res) => { return res.ok(req.body); @@ -349,11 +260,11 @@ test('invalid body', async () => { router.post( { path: '/', - validate: schema => ({ + validate: { body: schema.object({ bar: schema.number(), }), - }), + }, }, (req, res) => { return res.ok(req.body); @@ -371,7 +282,9 @@ test('invalid body', async () => { .expect(400) .then(res => { expect(res.body).toEqual({ - error: '[bar]: expected value of type [number] but got [string]', + error: 'Bad Request', + statusCode: 400, + message: '[request body.bar]: expected value of type [number] but got [string]', }); }); }); @@ -382,11 +295,11 @@ test('handles putting', async () => { router.put( { path: '/', - validate: schema => ({ + validate: { body: schema.object({ key: schema.string(), }), - }), + }, }, (req, res) => { return res.ok(req.body); @@ -413,11 +326,11 @@ test('handles deleting', async () => { router.delete( { path: '/{id}', - validate: schema => ({ + validate: { params: schema.object({ id: schema.number(), }), - }), + }, }, (req, res) => { return res.ok({ key: req.params.id }); @@ -579,78 +492,12 @@ test('returns server and connection options on start', async () => { expect(innerServer).toBe((server as any).server); }); -test('registers registerOnPostAuth interceptor several times', async () => { - const { registerOnPostAuth } = await server.setup(config); - const doRegister = () => registerOnPostAuth(() => null as any); - - doRegister(); - expect(doRegister).not.toThrowError(); -}); - test('throws an error if starts without set up', async () => { await expect(server.start()).rejects.toThrowErrorMatchingInlineSnapshot( `"Http server is not setup up yet"` ); }); -test('enables auth for a route by default if registerAuth has been called', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => - res.ok({ authRequired: req.route.options.authRequired }) - ); - registerRouter(router); - - const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated()); - registerAuth(authenticate); - - await server.start(); - await supertest(innerServer.listener) - .get('/') - .expect(200, { authRequired: true }); - - expect(authenticate).toHaveBeenCalledTimes(1); -}); - -test('supports disabling auth for a route explicitly', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - - const router = new Router(''); - router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => - res.ok({ authRequired: req.route.options.authRequired }) - ); - registerRouter(router); - const authenticate = jest.fn(); - registerAuth(authenticate); - - await server.start(); - await supertest(innerServer.listener) - .get('/') - .expect(200, { authRequired: false }); - - expect(authenticate).toHaveBeenCalledTimes(0); -}); - -test('supports enabling auth for a route explicitly', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - - const router = new Router(''); - router.get({ path: '/', validate: false, options: { authRequired: true } }, (req, res) => - res.ok({ authRequired: req.route.options.authRequired }) - ); - registerRouter(router); - const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated({})); - await registerAuth(authenticate); - - await server.start(); - await supertest(innerServer.listener) - .get('/') - .expect(200, { authRequired: true }); - - expect(authenticate).toHaveBeenCalledTimes(1); -}); - test('allows attaching metadata to attach meta-data tag strings to a route', async () => { const tags = ['my:tag']; const { registerRouter, server: innerServer } = await server.setup(config); @@ -710,214 +557,6 @@ describe('setup contract', () => { expect(create()).rejects.toThrowError('A cookieSessionStorageFactory was already created'); }); }); - describe('#registerAuth', () => { - it('registers auth request interceptor only once', async () => { - const { registerAuth } = await server.setup(config); - const doRegister = () => registerAuth(() => null as any); - - doRegister(); - expect(doRegister).toThrowError('Auth interceptor was already registered'); - }); - - it('may grant access to a resource', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - registerRouter(router); - - await registerAuth((req, t) => t.authenticated()); - await server.start(); - - await supertest(innerServer.listener) - .get('/') - .expect(200, { content: 'ok' }); - }); - - it('supports rejecting a request from an unauthenticated user', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - registerRouter(router); - - await registerAuth((req, t) => t.rejected(Boom.unauthorized())); - await server.start(); - - await supertest(innerServer.listener) - .get('/') - .expect(401); - }); - - it('supports redirecting', async () => { - const redirectTo = '/redirect-url'; - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - registerRouter(router); - - await registerAuth((req, t) => { - return t.redirected(redirectTo); - }); - await server.start(); - - const response = await supertest(innerServer.listener) - .get('/') - .expect(302); - expect(response.header.location).toBe(redirectTo); - }); - - it(`doesn't expose internal error details`, async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - registerRouter(router); - - await registerAuth((req, t) => { - throw new Error('sensitive info'); - }); - await server.start(); - - await supertest(innerServer.listener) - .get('/') - .expect({ - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred', - }); - }); - - it('allows manipulating cookies via cookie session storage', async () => { - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - - const { - createCookieSessionStorageFactory, - registerAuth, - registerRouter, - server: innerServer, - } = await server.setup(config); - const sessionStorageFactory = await createCookieSessionStorageFactory( - cookieOptions - ); - registerAuth((req, t) => { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + 1000 }); - return t.authenticated({ state: user }); - }); - registerRouter(router); - await server.start(); - - const response = await supertest(innerServer.listener) - .get('/') - .expect(200, { content: 'ok' }); - - expect(response.header['set-cookie']).toBeDefined(); - const cookies = response.header['set-cookie']; - expect(cookies).toHaveLength(1); - - const sessionCookie = request.cookie(cookies[0]); - if (!sessionCookie) { - throw new Error('session cookie expected to be defined'); - } - expect(sessionCookie).toBeDefined(); - expect(sessionCookie.key).toBe('sid'); - expect(sessionCookie.value).toBeDefined(); - expect(sessionCookie.path).toBe('/'); - expect(sessionCookie.httpOnly).toBe(true); - }); - - it('allows manipulating cookies from route handler', async () => { - const { - createCookieSessionStorageFactory, - registerAuth, - registerRouter, - server: innerServer, - } = await server.setup(config); - const sessionStorageFactory = await createCookieSessionStorageFactory( - cookieOptions - ); - registerAuth((req, t) => { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + 1000 }); - return t.authenticated(); - }); - - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => res.ok({ content: 'ok' })); - router.get({ path: '/with-cookie', validate: false }, (req, res) => { - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.clear(); - return res.ok({ content: 'ok' }); - }); - registerRouter(router); - - await server.start(); - - const responseToSetCookie = await supertest(innerServer.listener) - .get('/') - .expect(200); - - expect(responseToSetCookie.header['set-cookie']).toBeDefined(); - - const responseToResetCookie = await supertest(innerServer.listener) - .get('/with-cookie') - .expect(200); - - expect(responseToResetCookie.header['set-cookie']).toEqual([ - 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', - ]); - }); - - it.skip('is the only place with access to the authorization header', async () => { - const token = 'Basic: user:password'; - const { - registerAuth, - registerOnPreAuth, - registerOnPostAuth, - registerRouter, - server: innerServer, - } = await server.setup(config); - - let fromRegisterOnPreAuth; - await registerOnPreAuth((req, t) => { - fromRegisterOnPreAuth = req.headers.authorization; - return t.next(); - }); - - let fromRegisterAuth; - await registerAuth((req, t) => { - fromRegisterAuth = req.headers.authorization; - return t.authenticated(); - }); - - let fromRegisterOnPostAuth; - await registerOnPostAuth((req, t) => { - fromRegisterOnPostAuth = req.headers.authorization; - return t.next(); - }); - - let fromRouteHandler; - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => { - fromRouteHandler = req.headers.authorization; - return res.ok({ content: 'ok' }); - }); - registerRouter(router); - - await server.start(); - - await supertest(innerServer.listener) - .get('/') - .set('Authorization', token) - .expect(200); - - expect(fromRegisterOnPreAuth).toEqual({}); - expect(fromRegisterAuth).toEqual({ authorization: token }); - expect(fromRegisterOnPostAuth).toEqual({}); - expect(fromRouteHandler).toEqual({}); - }); - }); describe('#auth.isAuthenticated()', () => { it('returns true if has been authorized', async () => { @@ -931,7 +570,7 @@ describe('setup contract', () => { ); registerRouter(router); - await registerAuth((req, t) => t.authenticated()); + await registerAuth((req, res, toolkit) => toolkit.authenticated()); await server.start(); await supertest(innerServer.listener) @@ -950,7 +589,7 @@ describe('setup contract', () => { ); registerRouter(router); - await registerAuth((req, t) => t.authenticated()); + await registerAuth((req, res, toolkit) => toolkit.authenticated()); await server.start(); await supertest(innerServer.listener) @@ -985,9 +624,9 @@ describe('setup contract', () => { auth, } = await server.setup(config); const sessionStorageFactory = await createCookieSessionStorageFactory(cookieOptions); - registerAuth((req, t) => { + registerAuth((req, res, toolkit) => { sessionStorageFactory.asScoped(req).set({ value: user, expires: Date.now() + 1000 }); - return t.authenticated({ state: user }); + return toolkit.authenticated({ state: user }); }); const router = new Router(''); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 4ca76d405a1f..bf529ea7875d 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Request, Server } from 'hapi'; +import { Request, Server, ResponseToolkit } from 'hapi'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; @@ -25,21 +25,98 @@ import { createServer, getListenerOptions, getServerOptions } from './http_tools import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; -import { Router, KibanaRequest } from './router'; +import { KibanaRequest, LegacyRequest, ResponseHeaders, Router } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, } from './cookie_session_storage'; import { SessionStorageFactory } from './session_storage'; -import { AuthStateStorage } from './auth_state_storage'; -import { AuthHeadersStorage } from './auth_headers_storage'; +import { AuthStateStorage, GetAuthState, IsAuthenticated } from './auth_state_storage'; +import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage'; import { BasePath } from './base_path_service'; +/** + * Kibana HTTP Service provides own abstraction for work with HTTP stack. + * Plugins don't have direct access to `hapi` server and its primitives anymore. Moreover, + * plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. + * This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. + * If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. + * + * @example + * To handle an incoming request in your plugin you should: + * - Create a `Router` instance. Use `plugin-id` as a prefix path segment for your routes. + * ```ts + * import { Router } from 'src/core/server'; + * const router = new Router('my-app'); + * ``` + * + * - Use `@kbn/config-schema` package to create a schema to validate the request `params`, `query`, and `body`. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error without calling the route's handler. + * To opt out of validating the request, specify `false`. + * ```ts + * import { schema, TypeOf } from '@kbn/config-schema'; + * const validate = { + * params: schema.object({ + * id: schema.string(), + * }), + * }; + * ``` + * + * - Declare a function to respond to incoming request. + * The function will receive `request` object containing request details: url, headers, matched route, as well as validated `params`, `query`, `body`. + * And `response` object instructing HTTP server to create HTTP response with information sent back to the client as the response body, headers, and HTTP status. + * Unlike, `hapi` route handler in the Legacy platform, any exception raised during the handler call will generate `500 Server error` response and log error details for further investigation. See below for returning custom error responses. + * ```ts + * const handler = async (request: KibanaRequest, response: ResponseFactory) => { + * const data = await findObject(request.params.id); + * // creates a command to respond with 'not found' error + * if (!data) return response.notFound(); + * // creates a command to send found data to the client and set response headers + * return response.ok(data, { + * headers: { + * 'content-type': 'application/json' + * } + * }); + * } + * ``` + * + * - Register route handler for GET request to 'my-app/path/{id}' path + * ```ts + * import { schema, TypeOf } from '@kbn/config-schema'; + * import { Router } from 'src/core/server'; + * const router = new Router('my-app'); + * + * const validate = { + * params: schema.object({ + * id: schema.string(), + * }), + * }; + * + * router.get({ + * path: 'path/{id}', + * validate + * }, + * async (request, response) => { + * const data = await findObject(request.params.id); + * if (!data) return response.notFound(); + * return response.ok(data, { + * headers: { + * 'content-type': 'application/json' + * } + * }); + * }); + * ``` + * @public + */ export interface HttpServerSetup { server: Server; + /** + * Add all the routes registered with `router` to HTTP server request listeners. + * @param router {@link Router} - a router with registered route handlers. + */ registerRouter: (router: Router) => void; /** * Creates cookie based session storage factory {@link SessionStorageFactory} + * @param cookieOptions {@link SessionStorageCookieOptions} - options to configure created cookie session storage. */ createCookieSessionStorageFactory: ( cookieOptions: SessionStorageCookieOptions @@ -49,35 +126,53 @@ export interface HttpServerSetup { * A handler should return a state to associate with the incoming request. * The state can be retrieved later via http.auth.get(..) * Only one AuthenticationHandler can be registered. + * @param handler {@link AuthenticationHandler} - function to perform authentication. */ registerAuth: (handler: AuthenticationHandler) => void; /** * To define custom logic to perform for incoming requests. Runs the handler before Auth - * hook performs a check that user has access to requested resources, so it's the only + * interceptor performs a check that user has access to requested resources, so it's the only * place when you can forward a request to another URL right on the server. * Can register any number of registerOnPostAuth, which are called in sequence * (from the first registered to the last). + * @param handler {@link OnPreAuthHandler} - function to call. */ registerOnPreAuth: (handler: OnPreAuthHandler) => void; /** - * To define custom logic to perform for incoming requests. Runs the handler after Auth hook + * To define custom logic to perform for incoming requests. Runs the handler after Auth interceptor * did make sure a user has access to the requested resource. * The auth state is available at stage via http.auth.get(..) * Can register any number of registerOnPreAuth, which are called in sequence * (from the first registered to the last). + * @param handler {@link OnPostAuthHandler} - function to call. */ registerOnPostAuth: (handler: OnPostAuthHandler) => void; basePath: { - get: (request: KibanaRequest | Request) => string; - set: (request: KibanaRequest | Request, basePath: string) => void; + /** + * returns `basePath` value, specific for an incoming request. + */ + get: (request: KibanaRequest | LegacyRequest) => string; + /** + * sets `basePath` value, specific for an incoming request. + */ + set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; + /** + * returns a new `basePath` value, prefixed with passed `url`. + */ prepend: (url: string) => string; + /** + * returns a new `basePath` value, cleaned up from passed `url`. + */ remove: (url: string) => string; }; auth: { - get: AuthStateStorage['get']; - isAuthenticated: AuthStateStorage['isAuthenticated']; - getAuthHeaders: AuthHeadersStorage['get']; + get: GetAuthState; + isAuthenticated: IsAuthenticated; + getAuthHeaders: GetAuthHeaders; }; + /** + * Flag showing whether a server was configured to use TLS connection. + */ isTlsEnabled: boolean; } @@ -90,11 +185,13 @@ export class HttpServer { private readonly log: Logger; private readonly authState: AuthStateStorage; - private readonly authHeaders: AuthHeadersStorage; + private readonly authRequestHeaders: AuthHeadersStorage; + private readonly authResponseHeaders: AuthHeadersStorage; constructor(private readonly logger: LoggerFactory, private readonly name: string) { this.authState = new AuthStateStorage(() => this.authRegistered); - this.authHeaders = new AuthHeadersStorage(); + this.authRequestHeaders = new AuthHeadersStorage(); + this.authResponseHeaders = new AuthHeadersStorage(); this.log = logger.get('http', 'server', name); } @@ -131,7 +228,7 @@ export class HttpServer { auth: { get: this.authState.get, isAuthenticated: this.authState.isAuthenticated, - getAuthHeaders: this.authHeaders.get, + getAuthHeaders: this.authRequestHeaders.get, }, isTlsEnabled: config.ssl.enabled, // Return server instance with the connection options so that we can properly @@ -151,7 +248,8 @@ export class HttpServer { for (const route of router.getRoutes()) { const { authRequired = true, tags } = route.options; this.server.route({ - handler: route.handler, + handler: (req: Request, responseToolkit: ResponseToolkit) => + route.handler(req, responseToolkit, this.log), method: route.method, path: this.getRouteFullPath(router.path, route.path), options: { @@ -183,14 +281,14 @@ export class HttpServer { return; } - this.registerOnPreAuth((request, toolkit) => { + this.registerOnPreAuth((request, response, toolkit) => { const oldUrl = request.url.href!; const newURL = basePathService.remove(oldUrl); const shouldRedirect = newURL !== oldUrl; if (shouldRedirect) { - return toolkit.redirected(newURL, { forward: true }); + return toolkit.rewriteUrl(newURL); } - return toolkit.rejected(new Error('not found'), { statusCode: 404 }); + return response.notFound(new Error('not found')); }); } @@ -206,7 +304,7 @@ export class HttpServer { throw new Error('Server is not created yet'); } - this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn)); + this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn, this.log)); } private registerOnPreAuth(fn: OnPreAuthHandler) { @@ -214,7 +312,7 @@ export class HttpServer { throw new Error('Server is not created yet'); } - this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn)); + this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log)); } private async createCookieSessionStorageFactory( @@ -247,13 +345,24 @@ export class HttpServer { this.authRegistered = true; this.server.auth.scheme('login', () => ({ - authenticate: adoptToHapiAuthFormat(fn, (req, { state, headers }) => { - this.authState.set(req, state); - this.authHeaders.set(req, headers); - // we mutate headers only for the backward compatibility with the legacy platform. - // where some plugin read directly from headers to identify whether a user is authenticated. - Object.assign(req.headers, headers); - }), + authenticate: adoptToHapiAuthFormat( + fn, + this.log, + (req, { state, requestHeaders, responseHeaders }) => { + this.authState.set(req, state); + + if (responseHeaders) { + this.authResponseHeaders.set(req, responseHeaders); + } + + if (requestHeaders) { + this.authRequestHeaders.set(req, requestHeaders); + // we mutate headers only for the backward compatibility with the legacy platform. + // where some plugin read directly from headers to identify whether a user is authenticated. + Object.assign(req.headers, requestHeaders); + } + } + ), })); this.server.auth.strategy('session', 'login'); @@ -262,5 +371,40 @@ export class HttpServer { // should be applied for all routes if they don't specify auth strategy in route declaration // https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions this.server.auth.default('session'); + + this.server.ext('onPreResponse', (request, t) => { + const authResponseHeaders = this.authResponseHeaders.get(request); + this.extendResponseWithHeaders(request, authResponseHeaders); + return t.continue; + }); + } + + private extendResponseWithHeaders(request: Request, headers?: ResponseHeaders) { + const response = request.response; + if (!headers || !response) return; + + if (response instanceof Error) { + this.findHeadersIntersection(response.output.headers, headers); + // hapi wraps all error response in Boom object internally + response.output.headers = { + ...response.output.headers, + ...(headers as any), // hapi types don't specify string[] as valid value + }; + } else { + for (const [headerName, headerValue] of Object.entries(headers)) { + this.findHeadersIntersection(response.headers, headers); + response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value + } + } + } + + // NOTE: responseHeaders contains not a full list of response headers, but only explicitly set on a response object. + // any headers added by hapi internally, like `content-type`, `content-length`, etc. do not present here. + private findHeadersIntersection(responseHeaders: ResponseHeaders, headers: ResponseHeaders) { + Object.keys(headers).forEach(headerName => { + if (responseHeaders[headerName] !== undefined) { + this.log.warn(`Server rewrites a response header [${headerName}].`); + } + }); } } diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 02103fc4acc8..e3a62c27d6a5 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -18,12 +18,9 @@ */ import { Server } from 'hapi'; -import { HttpService } from './http_service'; -import { HttpServerSetup } from './http_server'; -import { HttpServiceSetup } from './http_service'; +import { HttpService, HttpServiceSetup } from './http_service'; import { OnPreAuthToolkit } from './lifecycle/on_pre_auth'; import { AuthToolkit } from './lifecycle/auth'; -import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; import { sessionStorageMock } from './cookie_session_storage.mocks'; type ServiceSetupMockType = jest.Mocked & { @@ -52,10 +49,8 @@ const createSetupContractMock = () => { isAuthenticated: jest.fn(), getAuthHeaders: jest.fn(), }, - createNewServer: jest.fn(), isTlsEnabled: false, }; - setupContract.createNewServer.mockResolvedValue({} as HttpServerSetup); setupContract.createCookieSessionStorageFactory.mockResolvedValue( sessionStorageMock.createFactory() ); @@ -75,20 +70,11 @@ const createHttpServiceMock = () => { const createOnPreAuthToolkitMock = (): jest.Mocked => ({ next: jest.fn(), - redirected: jest.fn(), - rejected: jest.fn(), + rewriteUrl: jest.fn(), }); const createAuthToolkitMock = (): jest.Mocked => ({ authenticated: jest.fn(), - redirected: jest.fn(), - rejected: jest.fn(), -}); - -const createOnPostAuthToolkitMock = (): jest.Mocked => ({ - next: jest.fn(), - redirected: jest.fn(), - rejected: jest.fn(), }); export const httpServiceMock = { @@ -97,5 +83,4 @@ export const httpServiceMock = { createSetupContract: createSetupContractMock, createOnPreAuthToolkit: createOnPreAuthToolkitMock, createAuthToolkit: createAuthToolkitMock, - createOnPostAuthToolkit: createOnPostAuthToolkitMock, }; diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index f003ba131443..6ed19d6f97e8 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -30,6 +30,7 @@ import { getEnvOptions } from '../config/__mocks__/env'; const logger = loggingServiceMock.create(); const env = Env.createDefault(getEnvOptions()); +const coreId = Symbol(); const createConfigService = (value: Partial = {}) => { const configService = new ConfigService( @@ -68,7 +69,7 @@ test('creates and sets up http server', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); expect(mockHttpServer.mock.instances.length).toBe(1); @@ -103,6 +104,7 @@ test('spins up notReady server until started if configured with `autoListen:true })); const service = new HttpService({ + coreId, configService, env: new Env('.', getEnvOptions()), logger, @@ -133,46 +135,6 @@ test('spins up notReady server until started if configured with `autoListen:true expect(notReadyHapiServer.stop).toBeCalledTimes(1); }); -// this is an integration test! -test('creates and sets up second http server', async () => { - const configService = createConfigService({ - host: 'localhost', - port: 1234, - }); - const { HttpServer } = jest.requireActual('./http_server'); - - mockHttpServer.mockImplementation((...args) => new HttpServer(...args)); - - const service = new HttpService({ configService, env, logger }); - const serverSetup = await service.setup(); - const cfg = { port: 2345 }; - await serverSetup.createNewServer(cfg); - const server = await service.start(); - expect(server.isListening()).toBeTruthy(); - expect(server.isListening(cfg.port)).toBeTruthy(); - - try { - await serverSetup.createNewServer(cfg); - } catch (err) { - expect(err.message).toBe('port 2345 is already in use'); - } - - try { - await serverSetup.createNewServer({ port: 1234 }); - } catch (err) { - expect(err.message).toBe('port 1234 is already in use'); - } - - try { - await serverSetup.createNewServer({ host: 'example.org' }); - } catch (err) { - expect(err.message).toBe('port must be defined'); - } - await service.stop(); - expect(server.isListening()).toBeFalsy(); - expect(server.isListening(cfg.port)).toBeFalsy(); -}); - test('logs error if already set up', async () => { const configService = createConfigService(); @@ -184,7 +146,7 @@ test('logs error if already set up', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); await service.setup(); @@ -202,7 +164,7 @@ test('stops http server', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); await service.setup(); await service.start(); @@ -229,7 +191,7 @@ test('stops not ready server if it is running', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); await service.setup(); @@ -252,7 +214,7 @@ test('register route handler', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); const router = new Router('/foo'); const { registerRouter } = await service.setup(); @@ -272,9 +234,8 @@ test('returns http server contract on setup', async () => { stop: noop, })); - const service = new HttpService({ configService, env, logger }); - const { createNewServer, ...setupHttpServer } = await service.setup(); - expect(createNewServer).toBeDefined(); + const service = new HttpService({ coreId, configService, env, logger }); + const setupHttpServer = await service.setup(); expect(setupHttpServer).toEqual(httpServer); }); @@ -289,6 +250,7 @@ test('does not start http server if process is dev cluster master', async () => mockHttpServer.mockImplementation(() => httpServer); const service = new HttpService({ + coreId, configService, env: new Env('.', getEnvOptions({ isDevClusterMaster: true })), logger, @@ -313,6 +275,7 @@ test('does not start http server if configured with `autoListen:false`', async ( mockHttpServer.mockImplementation(() => httpServer); const service = new HttpService({ + coreId, configService, env: new Env('.', getEnvOptions()), logger, diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index b06c690cf262..e69906d512ba 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -25,14 +25,12 @@ import { LoggerFactory } from '../logging'; import { CoreService } from '../../types'; import { Logger } from '../logging'; import { CoreContext } from '../core_context'; -import { HttpConfig, HttpConfigType, config as httpConfig } from './http_config'; +import { HttpConfig, HttpConfigType } from './http_config'; import { HttpServer, HttpServerSetup } from './http_server'; import { HttpsRedirectServer } from './https_redirect_server'; /** @public */ -export interface HttpServiceSetup extends HttpServerSetup { - createNewServer: (cfg: Partial) => Promise; -} +export type HttpServiceSetup = HttpServerSetup; /** @public */ export interface HttpServiceStart { /** Indicates if http server is listening on a given port */ @@ -42,7 +40,6 @@ export interface HttpServiceStart { /** @internal */ export class HttpService implements CoreService { private readonly httpServer: HttpServer; - private readonly secondaryServers: Map = new Map(); private readonly httpsRedirectServer: HttpsRedirectServer; private readonly config$: Observable; private configSubscription?: Subscription; @@ -77,17 +74,11 @@ export class HttpService implements CoreService server.start())); } return { - isListening: (port: number = 0) => { - const server = this.secondaryServers.get(port); - if (server) return server.isListening(); - return this.httpServer.isListening(); - }, + isListening: () => this.httpServer.isListening(), }; } @@ -129,32 +115,6 @@ export class HttpService implements CoreService) { - const { port } = cfg; - const config = await this.config$.pipe(first()).toPromise(); - - if (!port) { - throw new Error('port must be defined'); - } - - // verify that main server and none of the secondary servers are already using this port - if (this.secondaryServers.has(port) || config.port === port) { - throw new Error(`port ${port} is already in use`); - } - - for (const [key, val] of Object.entries(cfg)) { - httpConfig.schema.validateKey(key, val); - } - - const baseConfig = await this.config$.pipe(first()).toPromise(); - const finalConfig = { ...baseConfig, ...cfg }; - - const httpServer = new HttpServer(this.logger, `secondary server:${port}`); - const httpSetup = await httpServer.setup(finalConfig); - this.secondaryServers.set(port, httpServer); - return httpSetup; - } - public async stop() { if (this.configSubscription === undefined) { return; @@ -168,8 +128,6 @@ export class HttpService implements CoreService s.stop())); - this.secondaryServers.clear(); } private async runNotReadyServer(config: HttpConfig) { @@ -177,7 +135,7 @@ export class HttpService implements CoreService { const logger = loggingServiceMock.create(); const server = new HttpServer(logger, 'foo'); - test('returns 408 on timeout error', async () => { + test('closes sockets on timeout', async () => { const router = new Router(''); router.get({ path: '/a', validate: false }, async (req, res) => { await new Promise(resolve => setTimeout(resolve, 2000)); @@ -87,9 +87,8 @@ describe('timeouts', () => { await server.start(); - await supertest(innerServer.listener) - .get('/a') - .expect(408); + expect(supertest(innerServer.listener).get('/a')).rejects.toThrow('socket hang up'); + await supertest(innerServer.listener) .get('/b') .expect(200); diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 24c0264893d1..2953d5272ebe 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -97,11 +97,7 @@ export function createServer(serverOptions: ServerOptions, listenerOptions: List server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout; server.listener.setTimeout(listenerOptions.socketTimeout); server.listener.on('timeout', socket => { - if (socket.writable) { - socket.end(Buffer.from('HTTP/1.1 408 Request Timeout\r\n\r\n', 'ascii')); - } else { - socket.destroy(); - } + socket.destroy(); }); server.listener.on('clientError', (err, socket) => { if (socket.writable) { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index e9c2425bc82c..0fe827360ffd 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -19,17 +19,39 @@ export { config, HttpConfig, HttpConfigType } from './http_config'; export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service'; +export { HttpServerSetup } from './http_server'; export { GetAuthHeaders } from './auth_headers_storage'; +export { AuthStatus, GetAuthState, IsAuthenticated } from './auth_state_storage'; export { + CustomHttpResponseOptions, + IKibanaSocket, isRealRequest, + HttpResponseOptions, + HttpResponsePayload, KibanaRequest, KibanaRequestRoute, + KnownHeaders, + LegacyRequest, + LifecycleResponseFactory, + RedirectResponseOptions, + RequestHandler, + ResponseError, + ResponseErrorMeta, + kibanaResponseFactory, + KibanaResponseFactory, + RouteConfig, Router, RouteMethod, RouteConfigOptions, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; -export { AuthenticationHandler, AuthHeaders, AuthResultData, AuthToolkit } from './lifecycle/auth'; +export { + AuthenticationHandler, + AuthHeaders, + AuthResultParams, + AuthToolkit, +} from './lifecycle/auth'; export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; export { SessionStorageFactory, SessionStorage } from './session_storage'; +export { SessionStorageCookieOptions } from './cookie_session_storage'; diff --git a/src/core/server/http/integration_tests/http_service.test.mocks.ts b/src/core/server/http/integration_tests/core_service.test.mocks.ts similarity index 100% rename from src/core/server/http/integration_tests/http_service.test.mocks.ts rename to src/core/server/http/integration_tests/core_service.test.mocks.ts diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts new file mode 100644 index 000000000000..691b1d016871 --- /dev/null +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -0,0 +1,308 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 Boom from 'boom'; +import { Request } from 'hapi'; +import { first } from 'rxjs/operators'; +import { clusterClientMock } from './core_service.test.mocks'; + +import { Router } from '../router'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +interface User { + id: string; + roles?: string[]; +} + +interface StorageData { + value: User; + expires: number; +} + +describe('http service', () => { + describe('legacy server', () => { + describe('#registerAuth()', () => { + const sessionDurationMs = 1000; + const cookieOptions = { + name: 'sid', + encryptionKey: 'something_at_least_32_characters', + validate: (session: StorageData) => true, + isSecure: false, + path: '/', + }; + + let root: ReturnType; + beforeEach(async () => { + root = kbnTestServer.createRoot(); + }, 30000); + + afterEach(async () => { + clusterClientMock.mockClear(); + await root.shutdown(); + }); + + it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => { + const { http } = await root.setup(); + const sessionStorageFactory = await http.createCookieSessionStorageFactory( + cookieOptions + ); + http.registerAuth((req, res, toolkit) => { + if (req.headers.authorization) { + const user = { id: '42' }; + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); + return toolkit.authenticated({ state: user }); + } else { + return res.unauthorized(); + } + }); + await root.start(); + + const legacyUrl = '/legacy'; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyUrl, + handler: () => 'ok from legacy server', + }); + + const response = await kbnTestServer.request + .get(root, legacyUrl) + .expect(200, 'ok from legacy server'); + + expect(response.header['set-cookie']).toHaveLength(1); + }); + + it('passes authHeaders as request headers to the legacy platform', async () => { + const token = 'Basic: name:password'; + const { http } = await root.setup(); + const sessionStorageFactory = await http.createCookieSessionStorageFactory( + cookieOptions + ); + http.registerAuth((req, res, toolkit) => { + if (req.headers.authorization) { + const user = { id: '42' }; + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); + return toolkit.authenticated({ + state: user, + requestHeaders: { + authorization: token, + }, + }); + } else { + return res.unauthorized(); + } + }); + await root.start(); + + const legacyUrl = '/legacy'; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyUrl, + handler: (req: Request) => ({ + authorization: req.headers.authorization, + custom: req.headers.custom, + }), + }); + + await kbnTestServer.request + .get(root, legacyUrl) + .set({ custom: 'custom-header' }) + .expect(200, { authorization: token, custom: 'custom-header' }); + }); + + it('passes associated auth state to Legacy platform', async () => { + const user = { id: '42' }; + + const { http } = await root.setup(); + const sessionStorageFactory = await http.createCookieSessionStorageFactory( + cookieOptions + ); + http.registerAuth((req, res, toolkit) => { + if (req.headers.authorization) { + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); + return toolkit.authenticated({ state: user }); + } else { + return res.unauthorized(); + } + }); + await root.start(); + + const legacyUrl = '/legacy'; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyUrl, + handler: kbnServer.newPlatform.setup.core.http.auth.get, + }); + + const response = await kbnTestServer.request.get(root, legacyUrl).expect(200); + expect(response.body.state).toEqual(user); + expect(response.body.status).toEqual('authenticated'); + + expect(response.header['set-cookie']).toHaveLength(1); + }); + + it('attach security header to a successful response handled by Legacy platform', async () => { + const authResponseHeader = { + 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', + }; + const { http } = await root.setup(); + const { registerAuth } = http; + + await registerAuth((req, res, toolkit) => { + return toolkit.authenticated({ responseHeaders: authResponseHeader }); + }); + + await root.start(); + + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: '/legacy', + handler: () => 'ok', + }); + + const response = await kbnTestServer.request.get(root, '/legacy').expect(200); + expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); + }); + + it('attach security header to an error response handled by Legacy platform', async () => { + const authResponseHeader = { + 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', + }; + const { http } = await root.setup(); + const { registerAuth } = http; + + await registerAuth((req, res, toolkit) => { + return toolkit.authenticated({ responseHeaders: authResponseHeader }); + }); + + await root.start(); + + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: '/legacy', + handler: () => { + throw Boom.badRequest(); + }, + }); + + const response = await kbnTestServer.request.get(root, '/legacy').expect(400); + expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); + }); + }); + + describe('#basePath()', () => { + let root: ReturnType; + beforeEach(async () => { + root = kbnTestServer.createRoot(); + }, 30000); + + afterEach(async () => await root.shutdown()); + it('basePath information for an incoming request is available in legacy server', async () => { + const reqBasePath = '/requests-specific-base-path'; + const { http } = await root.setup(); + http.registerOnPreAuth((req, res, toolkit) => { + http.basePath.set(req, reqBasePath); + return toolkit.next(); + }); + + await root.start(); + + const legacyUrl = '/legacy'; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyUrl, + handler: kbnServer.newPlatform.setup.core.http.basePath.get, + }); + + await kbnTestServer.request.get(root, legacyUrl).expect(200, reqBasePath); + }); + }); + }); + describe('elasticsearch', () => { + let root: ReturnType; + beforeEach(async () => { + root = kbnTestServer.createRoot(); + }, 30000); + + afterEach(async () => { + clusterClientMock.mockClear(); + await root.shutdown(); + }); + it('rewrites authorization header via authHeaders to make a request to Elasticsearch', async () => { + const authHeaders = { authorization: 'Basic: user:password' }; + const { http, elasticsearch } = await root.setup(); + const { registerAuth, registerRouter } = http; + + await registerAuth((req, res, toolkit) => + toolkit.authenticated({ requestHeaders: authHeaders }) + ); + + const router = new Router('/new-platform'); + router.get({ path: '/', validate: false }, async (req, res) => { + const client = await elasticsearch.dataClient$.pipe(first()).toPromise(); + client.asScoped(req); + return res.ok({ header: 'ok' }); + }); + registerRouter(router); + + await root.start(); + + await kbnTestServer.request.get(root, '/new-platform/').expect(200); + expect(clusterClientMock).toBeCalledTimes(1); + const [firstCall] = clusterClientMock.mock.calls; + const [, , headers] = firstCall; + expect(headers).toEqual(authHeaders); + }); + + it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => { + const authorizationHeader = 'Basic: username:password'; + const { http, elasticsearch } = await root.setup(); + const { registerRouter } = http; + + const router = new Router('/new-platform'); + router.get({ path: '/', validate: false }, async (req, res) => { + const client = await elasticsearch.dataClient$.pipe(first()).toPromise(); + client.asScoped(req); + return res.ok({ header: 'ok' }); + }); + registerRouter(router); + + await root.start(); + + await kbnTestServer.request + .get(root, '/new-platform/') + .set('Authorization', authorizationHeader) + .expect(200); + + expect(clusterClientMock).toBeCalledTimes(1); + const [firstCall] = clusterClientMock.mock.calls; + const [, , headers] = firstCall; + expect(headers).toEqual({ + authorization: authorizationHeader, + }); + }); + }); +}); diff --git a/src/core/server/http/integration_tests/http_service.test.ts b/src/core/server/http/integration_tests/http_service.test.ts deleted file mode 100644 index 3c3ee866ff55..000000000000 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ /dev/null @@ -1,378 +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 Boom from 'boom'; -import { Request } from 'hapi'; -import { first } from 'rxjs/operators'; -import { clusterClientMock } from './http_service.test.mocks'; - -import { Router } from '../router'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; - -interface User { - id: string; - roles?: string[]; -} - -interface StorageData { - value: User; - expires: number; -} - -describe('http service', () => { - describe('setup contract', () => { - describe('#registerAuth()', () => { - const sessionDurationMs = 1000; - const cookieOptions = { - name: 'sid', - encryptionKey: 'something_at_least_32_characters', - validate: (session: StorageData) => true, - isSecure: false, - path: '/', - }; - - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot(); - }, 30000); - - afterEach(async () => { - clusterClientMock.mockClear(); - await root.shutdown(); - }); - - it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => { - const { http } = await root.setup(); - const sessionStorageFactory = await http.createCookieSessionStorageFactory( - cookieOptions - ); - http.registerAuth((req, t) => { - if (req.headers.authorization) { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return t.authenticated({ state: user }); - } else { - return t.rejected(Boom.unauthorized()); - } - }); - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: () => 'ok from legacy server', - }); - - const response = await kbnTestServer.request - .get(root, legacyUrl) - .expect(200, 'ok from legacy server'); - - expect(response.header['set-cookie']).toHaveLength(1); - }); - - it('passes authHeaders as request headers to the legacy platform', async () => { - const token = 'Basic: name:password'; - const { http } = await root.setup(); - const sessionStorageFactory = await http.createCookieSessionStorageFactory( - cookieOptions - ); - http.registerAuth((req, t) => { - if (req.headers.authorization) { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return t.authenticated({ - state: user, - headers: { - authorization: token, - }, - }); - } else { - return t.rejected(Boom.unauthorized()); - } - }); - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: (req: Request) => ({ - authorization: req.headers.authorization, - custom: req.headers.custom, - }), - }); - - await kbnTestServer.request - .get(root, legacyUrl) - .set({ custom: 'custom-header' }) - .expect(200, { authorization: token, custom: 'custom-header' }); - }); - - it('passes associated auth state to Legacy platform', async () => { - const user = { id: '42' }; - - const { http } = await root.setup(); - const sessionStorageFactory = await http.createCookieSessionStorageFactory( - cookieOptions - ); - http.registerAuth((req, t) => { - if (req.headers.authorization) { - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return t.authenticated({ state: user }); - } else { - return t.rejected(Boom.unauthorized()); - } - }); - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: kbnServer.newPlatform.setup.core.http.auth.get, - }); - - const response = await kbnTestServer.request.get(root, legacyUrl).expect(200); - expect(response.body.state).toEqual(user); - expect(response.body.status).toEqual('authenticated'); - - expect(response.header['set-cookie']).toHaveLength(1); - }); - - it('rewrites authorization header via authHeaders to make a request to Elasticsearch', async () => { - const authHeaders = { authorization: 'Basic: user:password' }; - const { http, elasticsearch } = await root.setup(); - const { registerAuth, registerRouter } = http; - - await registerAuth((req, t) => { - return t.authenticated({ headers: authHeaders }); - }); - - const router = new Router('/new-platform'); - router.get({ path: '/', validate: false }, async (req, res) => { - const client = await elasticsearch.dataClient$.pipe(first()).toPromise(); - client.asScoped(req); - return res.ok({ header: 'ok' }); - }); - registerRouter(router); - - await root.start(); - - await kbnTestServer.request.get(root, '/new-platform/').expect(200); - expect(clusterClientMock).toBeCalledTimes(1); - const [firstCall] = clusterClientMock.mock.calls; - const [, , headers] = firstCall; - expect(headers).toEqual(authHeaders); - }); - - it('pass request authorization header to Elasticsearch if registerAuth was not set', async () => { - const authorizationHeader = 'Basic: username:password'; - const { http, elasticsearch } = await root.setup(); - const { registerRouter } = http; - - const router = new Router('/new-platform'); - router.get({ path: '/', validate: false }, async (req, res) => { - const client = await elasticsearch.dataClient$.pipe(first()).toPromise(); - client.asScoped(req); - return res.ok({ header: 'ok' }); - }); - registerRouter(router); - - await root.start(); - - await kbnTestServer.request - .get(root, '/new-platform/') - .set('Authorization', authorizationHeader) - .expect(200); - - expect(clusterClientMock).toBeCalledTimes(1); - const [firstCall] = clusterClientMock.mock.calls; - const [, , headers] = firstCall; - expect(headers).toEqual({ - authorization: authorizationHeader, - }); - }); - }); - - describe('#registerOnPostAuth()', () => { - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot(); - }, 30000); - afterEach(async () => await root.shutdown()); - - it('supports passing request through to the route handler', async () => { - const router = new Router('/new-platform'); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - - const { http } = await root.setup(); - http.registerOnPostAuth((req, t) => t.next()); - http.registerOnPostAuth(async (req, t) => { - await Promise.resolve(); - return t.next(); - }); - http.registerRouter(router); - await root.start(); - - await kbnTestServer.request.get(root, '/new-platform/').expect(200, { content: 'ok' }); - }); - - it('supports redirecting to configured url', async () => { - const redirectTo = '/redirect-url'; - const { http } = await root.setup(); - http.registerOnPostAuth(async (req, t) => t.redirected(redirectTo)); - await root.start(); - - const response = await kbnTestServer.request.get(root, '/new-platform/').expect(302); - expect(response.header.location).toBe(redirectTo); - }); - - it('fails a request with configured error and status code', async () => { - const { http } = await root.setup(); - http.registerOnPostAuth(async (req, t) => - t.rejected(new Error('unexpected error'), { statusCode: 400 }) - ); - await root.start(); - - await kbnTestServer.request - .get(root, '/new-platform/') - .expect(400, { statusCode: 400, error: 'Bad Request', message: 'unexpected error' }); - }); - - it(`doesn't expose internal error details`, async () => { - const { http } = await root.setup(); - http.registerOnPostAuth(async (req, t) => { - throw new Error('sensitive info'); - }); - await root.start(); - - await kbnTestServer.request.get(root, '/new-platform/').expect({ - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred', - }); - }); - - it(`doesn't share request object between interceptors`, async () => { - const { http } = await root.setup(); - http.registerOnPostAuth(async (req, t) => { - // @ts-ignore. don't complain customField is not defined on Request type - req.customField = { value: 42 }; - return t.next(); - }); - http.registerOnPostAuth((req, t) => { - // @ts-ignore don't complain customField is not defined on Request type - if (typeof req.customField !== 'undefined') { - throw new Error('Request object was mutated'); - } - return t.next(); - }); - const router = new Router('/new-platform'); - router.get({ path: '/', validate: false }, async (req, res) => - // @ts-ignore. don't complain customField is not defined on Request type - res.ok({ customField: String(req.customField) }) - ); - http.registerRouter(router); - await root.start(); - - await kbnTestServer.request - .get(root, '/new-platform/') - .expect(200, { customField: 'undefined' }); - }); - }); - - describe('#registerOnPostAuth() toolkit', () => { - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot(); - }, 30000); - - afterEach(async () => await root.shutdown()); - it('supports Url change on the flight', async () => { - const { http } = await root.setup(); - http.registerOnPreAuth((req, t) => { - return t.redirected('/new-platform/new-url', { forward: true }); - }); - - const router = new Router('/new-platform'); - router.get({ path: '/new-url', validate: false }, async (req, res) => - res.ok({ key: 'new-url-reached' }) - ); - http.registerRouter(router); - - await root.start(); - - await kbnTestServer.request.get(root, '/').expect(200, { key: 'new-url-reached' }); - }); - - it('url re-write works for legacy server as well', async () => { - const { http } = await root.setup(); - const newUrl = '/new-url'; - http.registerOnPreAuth((req, t) => { - return t.redirected(newUrl, { forward: true }); - }); - - await root.start(); - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: newUrl, - handler: () => 'ok-from-legacy', - }); - - await kbnTestServer.request.get(root, '/').expect(200, 'ok-from-legacy'); - }); - }); - - describe('#basePath()', () => { - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot(); - }, 30000); - - afterEach(async () => await root.shutdown()); - it('basePath information for an incoming request is available in legacy server', async () => { - const reqBasePath = '/requests-specific-base-path'; - const { http } = await root.setup(); - http.registerOnPreAuth((req, t) => { - http.basePath.set(req, reqBasePath); - return t.next(); - }); - - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: kbnServer.newPlatform.setup.core.http.basePath.get, - }); - - await kbnTestServer.request.get(root, legacyUrl).expect(200, reqBasePath); - }); - }); - }); -}); diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts new file mode 100644 index 000000000000..068e52ea3446 --- /dev/null +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -0,0 +1,923 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 supertest from 'supertest'; +import { ByteSizeValue } from '@kbn/config-schema'; +import request from 'request'; + +import { HttpConfig, Router } from '..'; +import { ensureRawRequest } from '../router'; +import { HttpServer } from '../http_server'; + +import { LoggerFactory } from '../../logging'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; + +let server: HttpServer; +let logger: LoggerFactory; + +const config = { + host: '127.0.0.1', + maxPayload: new ByteSizeValue(1024), + port: 10001, + ssl: { enabled: false }, +} as HttpConfig; + +interface User { + id: string; + roles?: string[]; +} + +interface StorageData { + value: User; + expires: number; +} + +beforeEach(() => { + logger = loggingServiceMock.create(); + server = new HttpServer(logger, 'tests'); +}); + +afterEach(async () => { + await server.stop(); +}); + +describe('OnPreAuth', () => { + it('supports registering request inceptors', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); + + const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + const callingOrder: string[] = []; + registerOnPreAuth((req, res, t) => { + callingOrder.push('first'); + return t.next(); + }); + + registerOnPreAuth((req, res, t) => { + callingOrder.push('second'); + return t.next(); + }); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, 'ok'); + + expect(callingOrder).toEqual(['first', 'second']); + }); + + it('supports request forwarding to specified url', async () => { + const router = new Router('/'); + + router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial')); + router.get({ path: '/redirectUrl', validate: false }, (req, res) => res.ok('redirected')); + + const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + let urlBeforeForwarding; + registerOnPreAuth((req, res, t) => { + urlBeforeForwarding = ensureRawRequest(req).raw.req.url; + return t.rewriteUrl('/redirectUrl'); + }); + + let urlAfterForwarding; + registerOnPreAuth((req, res, t) => { + // used by legacy platform + urlAfterForwarding = ensureRawRequest(req).raw.req.url; + return t.next(); + }); + + await server.start(); + + await supertest(innerServer.listener) + .get('/initial') + .expect(200, 'redirected'); + + expect(urlBeforeForwarding).toBe('/initial'); + expect(urlAfterForwarding).toBe('/redirectUrl'); + }); + + it('supports redirection from the interceptor', async () => { + const router = new Router('/'); + const redirectUrl = '/redirectUrl'; + router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial')); + + const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPreAuth((req, res, t) => + res.redirected(undefined, { + headers: { + location: redirectUrl, + }, + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/initial') + .expect(302); + + expect(result.header.location).toBe(redirectUrl); + }); + + it('supports rejecting request and adjusting response headers', async () => { + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); + + const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPreAuth((req, res, t) => + res.unauthorized('not found error', { + headers: { + 'www-authenticate': 'challenge', + }, + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.header['www-authenticate']).toBe('challenge'); + }); + + it("doesn't expose error details if interceptor throws", async () => { + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); + + const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPreAuth((req, res, t) => { + throw new Error('reason'); + }); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: reason], + ], + ] + `); + }); + + it('returns internal error if interceptor returns unexpected result', async () => { + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); + + const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPreAuth((req, res, t) => ({} as any)); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected result from OnPreAuth. Expected OnPreAuthResult or KibanaResponse, but given: [object Object].], + ], + ] + `); + }); + + it(`doesn't share request object between interceptors`, async () => { + const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); + registerOnPreAuth((req, res, t) => { + // @ts-ignore. don't complain customField is not defined on Request type + req.customField = { value: 42 }; + return t.next(); + }); + registerOnPreAuth((req, res, t) => { + // @ts-ignore don't complain customField is not defined on Request type + if (typeof req.customField !== 'undefined') { + throw new Error('Request object was mutated'); + } + return t.next(); + }); + const router = new Router('/'); + router.get({ path: '/', validate: false }, async (req, res) => + // @ts-ignore. don't complain customField is not defined on Request type + res.ok({ customField: String(req.customField) }) + ); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { customField: 'undefined' }); + }); +}); + +describe('OnPostAuth', () => { + it('supports registering request inceptors', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); + + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + const callingOrder: string[] = []; + registerOnPostAuth((req, res, t) => { + callingOrder.push('first'); + return t.next(); + }); + + registerOnPostAuth((req, res, t) => { + callingOrder.push('second'); + return t.next(); + }); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, 'ok'); + + expect(callingOrder).toEqual(['first', 'second']); + }); + + it('supports redirection from the interceptor', async () => { + const router = new Router('/'); + const redirectUrl = '/redirectUrl'; + router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial')); + + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPostAuth((req, res, t) => + res.redirected(undefined, { + headers: { + location: redirectUrl, + }, + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/initial') + .expect(302); + + expect(result.header.location).toBe(redirectUrl); + }); + + it('supports rejecting request and adjusting response headers', async () => { + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); + + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPostAuth((req, res, t) => + res.unauthorized('not found error', { + headers: { + 'www-authenticate': 'challenge', + }, + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.header['www-authenticate']).toBe('challenge'); + }); + + it("doesn't expose error details if interceptor throws", async () => { + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); + + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPostAuth((req, res, t) => { + throw new Error('reason'); + }); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: reason], + ], + ] + `); + }); + + it('returns internal error if interceptor returns unexpected result', async () => { + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); + + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPostAuth((req, res, t) => ({} as any)); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: [object Object].], + ], + ] + `); + }); + + it(`doesn't share request object between interceptors`, async () => { + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerOnPostAuth((req, res, t) => { + // @ts-ignore. don't complain customField is not defined on Request type + req.customField = { value: 42 }; + return t.next(); + }); + registerOnPostAuth((req, res, t) => { + // @ts-ignore don't complain customField is not defined on Request type + if (typeof req.customField !== 'undefined') { + throw new Error('Request object was mutated'); + } + return t.next(); + }); + const router = new Router('/'); + router.get({ path: '/', validate: false }, async (req, res) => + // @ts-ignore. don't complain customField is not defined on Request type + res.ok({ customField: String(req.customField) }) + ); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { customField: 'undefined' }); + }); +}); + +describe('Auth', () => { + const cookieOptions = { + name: 'sid', + encryptionKey: 'something_at_least_32_characters', + validate: () => true, + isSecure: false, + }; + + it('registers auth request interceptor only once', async () => { + const { registerAuth } = await server.setup(config); + const doRegister = () => registerAuth(() => null as any); + + doRegister(); + expect(doRegister).toThrowError('Auth interceptor was already registered'); + }); + + it('may grant access to a resource', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); + registerRouter(router); + + registerAuth((req, res, t) => t.authenticated()); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { content: 'ok' }); + }); + + it('enables auth for a route by default if registerAuth has been called', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => + res.ok({ authRequired: req.route.options.authRequired }) + ); + registerRouter(router); + + const authenticate = jest.fn().mockImplementation((req, res, t) => t.authenticated()); + registerAuth(authenticate); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { authRequired: true }); + + expect(authenticate).toHaveBeenCalledTimes(1); + }); + + test('supports disabling auth for a route explicitly', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => + res.ok({ authRequired: req.route.options.authRequired }) + ); + registerRouter(router); + const authenticate = jest.fn(); + registerAuth(authenticate); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { authRequired: false }); + + expect(authenticate).toHaveBeenCalledTimes(0); + }); + + test('supports enabling auth for a route explicitly', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, options: { authRequired: true } }, (req, res) => + res.ok({ authRequired: req.route.options.authRequired }) + ); + registerRouter(router); + const authenticate = jest.fn().mockImplementation((req, res, t) => t.authenticated({})); + await registerAuth(authenticate); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { authRequired: true }); + + expect(authenticate).toHaveBeenCalledTimes(1); + }); + + it('supports rejecting a request from an unauthenticated user', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); + registerRouter(router); + + registerAuth((req, res) => res.unauthorized()); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(401); + }); + + it('supports redirecting', async () => { + const redirectTo = '/redirect-url'; + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); + registerRouter(router); + + registerAuth((req, res) => + res.redirected(undefined, { + headers: { + location: redirectTo, + }, + }) + ); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(302); + expect(response.header.location).toBe(redirectTo); + }); + + it(`doesn't expose internal error details`, async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); + registerRouter(router); + + registerAuth((req, t) => { + throw new Error('reason'); + }); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: reason], + ], + ] + `); + }); + + it('allows manipulating cookies via cookie session storage', async () => { + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); + + const { + createCookieSessionStorageFactory, + registerAuth, + registerRouter, + server: innerServer, + } = await server.setup(config); + const sessionStorageFactory = await createCookieSessionStorageFactory( + cookieOptions + ); + registerAuth((req, res, toolkit) => { + const user = { id: '42' }; + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + 1000 }); + return toolkit.authenticated({ state: user }); + }); + registerRouter(router); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200, { content: 'ok' }); + + expect(response.header['set-cookie']).toBeDefined(); + const cookies = response.header['set-cookie']; + expect(cookies).toHaveLength(1); + + const sessionCookie = request.cookie(cookies[0]); + if (!sessionCookie) { + throw new Error('session cookie expected to be defined'); + } + expect(sessionCookie).toBeDefined(); + expect(sessionCookie.key).toBe('sid'); + expect(sessionCookie.value).toBeDefined(); + expect(sessionCookie.path).toBe('/'); + expect(sessionCookie.httpOnly).toBe(true); + }); + + it('allows manipulating cookies from route handler', async () => { + const { + createCookieSessionStorageFactory, + registerAuth, + registerRouter, + server: innerServer, + } = await server.setup(config); + const sessionStorageFactory = await createCookieSessionStorageFactory( + cookieOptions + ); + registerAuth((req, res, toolkit) => { + const user = { id: '42' }; + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + 1000 }); + return toolkit.authenticated(); + }); + + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => res.ok({ content: 'ok' })); + router.get({ path: '/with-cookie', validate: false }, (req, res) => { + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.clear(); + return res.ok({ content: 'ok' }); + }); + registerRouter(router); + + await server.start(); + + const responseToSetCookie = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(responseToSetCookie.header['set-cookie']).toBeDefined(); + + const responseToResetCookie = await supertest(innerServer.listener) + .get('/with-cookie') + .expect(200); + + expect(responseToResetCookie.header['set-cookie']).toEqual([ + 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', + ]); + }); + + it.skip('is the only place with access to the authorization header', async () => { + const token = 'Basic: user:password'; + const { + registerAuth, + registerOnPreAuth, + registerOnPostAuth, + registerRouter, + server: innerServer, + } = await server.setup(config); + + let fromRegisterOnPreAuth; + await registerOnPreAuth((req, res, toolkit) => { + fromRegisterOnPreAuth = req.headers.authorization; + return toolkit.next(); + }); + + let fromRegisterAuth; + registerAuth((req, res, toolkit) => { + fromRegisterAuth = req.headers.authorization; + return toolkit.authenticated(); + }); + + let fromRegisterOnPostAuth; + await registerOnPostAuth((req, res, toolkit) => { + fromRegisterOnPostAuth = req.headers.authorization; + return toolkit.next(); + }); + + let fromRouteHandler; + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => { + fromRouteHandler = req.headers.authorization; + return res.ok({ content: 'ok' }); + }); + registerRouter(router); + + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .set('Authorization', token) + .expect(200); + + expect(fromRegisterOnPreAuth).toEqual({}); + expect(fromRegisterAuth).toEqual({ authorization: token }); + expect(fromRegisterOnPostAuth).toEqual({}); + expect(fromRouteHandler).toEqual({}); + }); + + it('attach security header to a successful response', async () => { + const authResponseHeader = { + 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', + }; + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + registerAuth((req, res, toolkit) => { + return toolkit.authenticated({ responseHeaders: authResponseHeader }); + }); + + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok({ header: 'ok' })); + registerRouter(router); + + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); + }); + + it('attach security header to an error response', async () => { + const authResponseHeader = { + 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', + }; + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + registerAuth((req, res, toolkit) => { + return toolkit.authenticated({ responseHeaders: authResponseHeader }); + }); + + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.badRequest(new Error('reason'))); + registerRouter(router); + + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(400); + + expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); + }); + + it('logs warning if Auth Security Header rewrites response header for success response', async () => { + const authResponseHeader = { + 'www-authenticate': 'from auth interceptor', + }; + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + registerAuth((req, res, toolkit) => { + return toolkit.authenticated({ responseHeaders: authResponseHeader }); + }); + + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => + res.ok( + {}, + { + headers: { + 'www-authenticate': 'from handler', + }, + } + ) + ); + registerRouter(router); + + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(response.header['www-authenticate']).toBe('from auth interceptor'); + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Server rewrites a response header [www-authenticate].", + ], + ] + `); + }); + + it('logs warning if Auth Security Header rewrites response header for error response', async () => { + const authResponseHeader = { + 'www-authenticate': 'from auth interceptor', + }; + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + registerAuth((req, res, toolkit) => { + return toolkit.authenticated({ responseHeaders: authResponseHeader }); + }); + + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => + res.badRequest('reason', { + headers: { + 'www-authenticate': 'from handler', + }, + }) + ); + registerRouter(router); + + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(400); + + expect(response.header['www-authenticate']).toBe('from auth interceptor'); + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Server rewrites a response header [www-authenticate].", + ], + ] + `); + }); + + it('supports redirection from the interceptor', async () => { + const router = new Router('/'); + const redirectUrl = '/redirectUrl'; + router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial')); + + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPostAuth((req, res, t) => + res.redirected(undefined, { + headers: { + location: redirectUrl, + }, + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/initial') + .expect(302); + + expect(result.header.location).toBe(redirectUrl); + }); + + it('supports rejecting request and adjusting response headers', async () => { + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); + + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPostAuth((req, res, t) => + res.unauthorized('not found error', { + headers: { + 'www-authenticate': 'challenge', + }, + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.header['www-authenticate']).toBe('challenge'); + }); + + it("doesn't expose error details if interceptor throws", async () => { + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); + + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPostAuth((req, res, t) => { + throw new Error('reason'); + }); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: reason], + ], + ] + `); + }); + + it('returns internal error if interceptor returns unexpected result', async () => { + const router = new Router('/'); + router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); + + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerRouter(router); + + registerOnPostAuth((req, res, t) => ({} as any)); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: [object Object].], + ], + ] + `); + }); + it(`doesn't share request object between interceptors`, async () => { + const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + registerOnPostAuth((req, res, t) => { + // @ts-ignore. don't complain customField is not defined on Request type + req.customField = { value: 42 }; + return t.next(); + }); + registerOnPostAuth((req, res, t) => { + // @ts-ignore don't complain customField is not defined on Request type + if (typeof req.customField !== 'undefined') { + throw new Error('Request object was mutated'); + } + return t.next(); + }); + const router = new Router('/'); + router.get({ path: '/', validate: false }, async (req, res) => + // @ts-ignore. don't complain customField is not defined on Request type + res.ok({ customField: String(req.customField) }) + ); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { customField: 'undefined' }); + }); +}); diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts new file mode 100644 index 000000000000..5f3976c7c194 --- /dev/null +++ b/src/core/server/http/integration_tests/router.test.ts @@ -0,0 +1,1189 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Stream } from 'stream'; +import Boom from 'boom'; + +import supertest from 'supertest'; +import { ByteSizeValue, schema } from '@kbn/config-schema'; + +import { HttpConfig, Router } from '..'; +import { HttpServer } from '../http_server'; + +import { LoggerFactory } from '../../logging'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; + +let server: HttpServer; +let logger: LoggerFactory; + +const config = { + host: '127.0.0.1', + maxPayload: new ByteSizeValue(1024), + port: 10000, + ssl: { enabled: false }, +} as HttpConfig; + +beforeEach(() => { + logger = loggingServiceMock.create(); + server = new HttpServer(logger, 'tests'); +}); + +afterEach(async () => { + await server.stop(); +}); + +describe('Handler', () => { + it("Doesn't expose error details if handler throws", async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + throw new Error('unexpected error'); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: unexpected error], + ], + ] + `); + }); + + it('returns 500 Server error if handler throws Boom error', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + throw Boom.unauthorized(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unauthorized], + ], + ] + `); + }); + + it('returns 500 Server error if handler returns unexpected result', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => 'ok' as any); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected result from Route Handler. Expected KibanaResponse, but given: string.], + ], + ] + `); + }); + + it('returns 400 Bad request if request validation failed', async () => { + const router = new Router('/'); + + router.get( + { + path: '/', + validate: { + query: schema.object({ + page: schema.number(), + }), + }, + }, + (req, res) => res.noContent() + ); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .query({ page: 'one' }) + .expect(400); + + expect(result.body).toEqual({ + error: 'Bad Request', + message: '[request query.page]: expected value of type [number] but got [string]', + statusCode: 400, + }); + }); +}); + +describe('Response factory', () => { + describe('Success', () => { + it('supports answering with json object', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok({ key: 'value' }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.body).toEqual({ key: 'value' }); + expect(result.header['content-type']).toBe('application/json; charset=utf-8'); + }); + + it('supports answering with string', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('result'); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toBe('result'); + expect(result.header['content-type']).toBe('text/html; charset=utf-8'); + }); + + it('supports answering with undefined', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok(undefined); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200); + }); + + it('supports answering with Stream', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const stream = new Stream.Readable({ + read() { + this.push('a'); + this.push('b'); + this.push('c'); + this.push(null); + }, + }); + + return res.ok(stream); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toBe('abc'); + expect(result.header['content-type']).toBe(undefined); + }); + + it('supports answering with chunked Stream', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const stream = new Stream.PassThrough(); + stream.write('a'); + stream.write('b'); + setTimeout(function() { + stream.write('c'); + stream.end(); + }, 100); + + return res.ok(stream); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toBe('abc'); + expect(result.header['transfer-encoding']).toBe('chunked'); + }); + + it('supports answering with Buffer', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const buffer = Buffer.alloc(1028, '.'); + + return res.ok(buffer, { + headers: { + 'content-encoding': 'binary', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200) + .buffer(true); + + expect(result.header['content-encoding']).toBe('binary'); + expect(result.header['content-length']).toBe('1028'); + expect(result.header['content-type']).toBe('application/octet-stream'); + }); + + it('supports answering with Buffer text', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const buffer = new Buffer('abc'); + + return res.ok(buffer, { + headers: { + 'content-type': 'text/plain', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200) + .buffer(true); + + expect(result.text).toBe('abc'); + expect(result.header['content-length']).toBe('3'); + expect(result.header['content-type']).toBe('text/plain; charset=utf-8'); + }); + + it('supports configuring standard headers', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('value', { + headers: { + etag: '1234', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toEqual('value'); + expect(result.header.etag).toBe('1234'); + }); + + it('supports configuring non-standard headers', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('value', { + headers: { + etag: '1234', + 'x-kibana': 'key', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.text).toEqual('value'); + expect(result.header.etag).toBe('1234'); + expect(result.header['x-kibana']).toBe('key'); + }); + + it('accepted headers are case-insensitive.', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('value', { + headers: { + ETag: '1234', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.header.etag).toBe('1234'); + }); + + it('accept array of headers', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok('value', { + headers: { + 'set-cookie': ['foo', 'bar'], + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.header['set-cookie']).toEqual(['foo', 'bar']); + }); + + it('throws if given invalid json object as response payload', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const payload: any = { key: {} }; + payload.key.payload = payload; + return res.ok(payload); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(500); + + // error happens within hapi when route handler already finished execution. + expect(loggingServiceMock.collect(logger).error).toHaveLength(0); + }); + + it('200 OK with body', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.ok({ key: 'value' }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.body).toEqual({ key: 'value' }); + expect(result.header['content-type']).toBe('application/json; charset=utf-8'); + }); + + it('202 Accepted with body', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.accepted({ location: 'somewhere' }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(202); + + expect(result.body).toEqual({ location: 'somewhere' }); + expect(result.header['content-type']).toBe('application/json; charset=utf-8'); + }); + + it('204 No content', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.noContent(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(204); + + expect(result.noContent).toBe(true); + }); + }); + + describe('Redirection', () => { + it('302 supports redirection to configured URL', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.redirected('The document has moved', { + headers: { + location: '/new-url', + 'x-kibana': 'tag', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(302); + + expect(result.text).toBe('The document has moved'); + expect(result.header.location).toBe('/new-url'); + expect(result.header['x-kibana']).toBe('tag'); + }); + + it('throws if redirection url not provided', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.redirected(undefined, { + headers: { + 'x-kibana': 'tag', + }, + } as any); // location headers is required + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: expected 'location' header to be set], + ], + ] + `); + }); + }); + + describe('Error', () => { + it('400 Bad request', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('some message'); + return res.badRequest(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(400); + + expect(result.body).toEqual({ + error: 'Bad Request', + message: 'some message', + statusCode: 400, + }); + }); + + it('400 Bad request with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.badRequest(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(400); + + expect(result.body).toEqual({ + error: 'Bad Request', + message: 'Bad Request', + statusCode: 400, + }); + }); + + it('400 Bad request with additional data', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.badRequest({ message: 'some message', meta: { data: ['good', 'bad'] } }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(400); + + expect(result.body).toEqual({ + error: 'Bad Request', + message: 'some message', + meta: { + data: ['good', 'bad'], + }, + statusCode: 400, + }); + }); + + it('401 Unauthorized', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('no access'); + return res.unauthorized(error, { + headers: { + 'WWW-Authenticate': 'challenge', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.message).toBe('no access'); + expect(result.header['www-authenticate']).toBe('challenge'); + }); + + it('401 Unauthorized with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.unauthorized(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.message).toBe('Unauthorized'); + }); + + it('403 Forbidden', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('reason'); + return res.forbidden(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(403); + + expect(result.body.message).toBe('reason'); + }); + + it('403 Forbidden with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.forbidden(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(403); + + expect(result.body.message).toBe('Forbidden'); + }); + + it('404 Not Found', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('file is not found'); + return res.notFound(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(404); + + expect(result.body.message).toBe('file is not found'); + }); + + it('404 Not Found with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.notFound(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(404); + + expect(result.body.message).toBe('Not Found'); + }); + + it('409 Conflict', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('stale version'); + return res.conflict(error); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(409); + + expect(result.body.message).toBe('stale version'); + }); + + it('409 Conflict with default message', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.conflict(); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(409); + + expect(result.body.message).toBe('Conflict'); + }); + + it('Custom error response', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('some message'); + return res.customError(error, { + statusCode: 418, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(418); + + expect(result.body).toEqual({ + error: "I'm a teapot", + message: 'some message', + statusCode: 418, + }); + }); + + it('Custom error response for server error', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('some message'); + + return res.customError(error, { + statusCode: 500, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'some message', + statusCode: 500, + }); + }); + + it('Custom error response for Boom server error', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('some message'); + + return res.customError(Boom.boomify(error), { + statusCode: 500, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'some message', + statusCode: 500, + }); + }); + + it('Custom error response requires error status code', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('some message'); + return res.customError(error, { + statusCode: 200, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, + }); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected Http status code. Expected from 400 to 599, but given: 200], + ], + ] + `); + }); + }); + + describe('Custom', () => { + it('creates success response', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom(undefined, { + statusCode: 201, + headers: { + location: 'somewhere', + }, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(201); + + expect(result.header.location).toBe('somewhere'); + }); + + it('creates redirect response', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom('The document has moved', { + headers: { + location: '/new-url', + }, + statusCode: 301, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(301); + + expect(result.header.location).toBe('/new-url'); + }); + + it('throws if redirects without location header to be set', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom('The document has moved', { + headers: {}, + statusCode: 301, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: expected 'location' header to be set], + ], + ] + `); + }); + + it('creates error response', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('unauthorized'); + return res.custom(error, { + statusCode: 401, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.message).toBe('unauthorized'); + }); + + it('creates error response with additional data', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom( + { + message: 'unauthorized', + meta: { errorCode: 'K401' }, + }, + { + statusCode: 401, + } + ); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body).toEqual({ + error: 'Unauthorized', + message: 'unauthorized', + meta: { errorCode: 'K401' }, + statusCode: 401, + }); + }); + + it('creates error response with additional data and error object', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom( + { + message: new Error('unauthorized'), + meta: { errorCode: 'K401' }, + }, + { + statusCode: 401, + } + ); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body).toEqual({ + error: 'Unauthorized', + message: 'unauthorized', + meta: { errorCode: 'K401' }, + statusCode: 401, + }); + }); + + it('creates error response with Boom error', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = Boom.unauthorized(); + return res.custom(error, { + statusCode: 401, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.message).toBe('Unauthorized'); + }); + + it("Doesn't log details of created 500 Server error response", async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom('reason', { + statusCode: 500, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('reason'); + expect(loggingServiceMock.collect(logger).error).toHaveLength(0); + }); + + it('throws an error if not valid error is provided', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom( + { error: 'error-message' }, + { + statusCode: 401, + } + ); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: expected error message to be provided], + ], + ] + `); + }); + + it('throws if an error not provided', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + return res.custom(undefined, { + statusCode: 401, + }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: expected error message to be provided], + ], + ] + `); + }); + + it('throws an error if statusCode is not specified', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('error message'); + return res.custom(error, undefined as any); // options.statusCode is required + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: options.statusCode is expected to be set. given options: undefined], + ], + ] + `); + }); + + it('throws an error if statusCode is not valid', async () => { + const router = new Router('/'); + + router.get({ path: '/', validate: false }, (req, res) => { + const error = new Error('error message'); + return res.custom(error, { statusCode: 20 }); + }); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected Http status code. Expected from 100 to 599, but given: 20.], + ], + ] + `); + }); + }); +}); diff --git a/src/core/server/http/lifecycle/auth.test.ts b/src/core/server/http/lifecycle/auth.test.ts deleted file mode 100644 index 668d2a4fd11d..000000000000 --- a/src/core/server/http/lifecycle/auth.test.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 Boom from 'boom'; -import { adoptToHapiAuthFormat } from './auth'; -import { httpServerMock } from '../http_server.mocks'; - -describe('adoptToHapiAuthFormat', () => { - it('allows to associate arbitrary data with an incoming request', async () => { - const authData = { - state: { foo: 'bar' }, - headers: { authorization: 'baz' }, - }; - const authenticatedMock = jest.fn(); - const onSuccessMock = jest.fn(); - const onAuth = adoptToHapiAuthFormat((req, t) => t.authenticated(authData), onSuccessMock); - await onAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit({ - authenticated: authenticatedMock, - }) - ); - - expect(authenticatedMock).toBeCalledTimes(1); - expect(authenticatedMock).toBeCalledWith({ credentials: authData.state }); - - expect(onSuccessMock).toBeCalledTimes(1); - const [[, onSuccessData]] = onSuccessMock.mock.calls; - expect(onSuccessData).toEqual(authData); - }); - - it('Should allow redirecting to specified url', async () => { - const redirectUrl = '/docs'; - const onSuccessMock = jest.fn(); - const onAuth = adoptToHapiAuthFormat((req, t) => t.redirected(redirectUrl), onSuccessMock); - const takeoverSymbol = {}; - const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); - const result = await onAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit({ - redirect: redirectMock, - }) - ); - - expect(redirectMock).toBeCalledWith(redirectUrl); - expect(result).toBe(takeoverSymbol); - expect(onSuccessMock).not.toHaveBeenCalled(); - }); - - it('Should allow to specify statusCode and message for Boom error', async () => { - const onSuccessMock = jest.fn(); - const onAuth = adoptToHapiAuthFormat( - (req, t) => t.rejected(new Error('not found'), { statusCode: 404 }), - onSuccessMock - ); - const result = (await onAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit() - )) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe('not found'); - expect(result.output.statusCode).toBe(404); - expect(onSuccessMock).not.toHaveBeenCalled(); - }); - - it('Should return Boom.internal error error if interceptor throws', async () => { - const onAuth = adoptToHapiAuthFormat((req, t) => { - throw new Error('unknown error'); - }); - const result = (await onAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit() - )) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe('unknown error'); - expect(result.output.statusCode).toBe(500); - }); - - it('Should return Boom.internal error if interceptor returns unexpected result', async () => { - const onAuth = adoptToHapiAuthFormat(async (req, t) => undefined as any); - const result = (await onAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit() - )) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe( - 'Unexpected result from Authenticate. Expected AuthResult, but given: undefined.' - ); - expect(result.output.statusCode).toBe(500); - }); -}); diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 8319d52c2e88..a89c17a0340b 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -16,64 +16,37 @@ * specific language governing permissions and limitations * under the License. */ -import Boom from 'boom'; -import { noop } from 'lodash'; import { Lifecycle, Request, ResponseToolkit } from 'hapi'; -import { KibanaRequest } from '../router'; +import { Logger } from '../../logging'; +import { + HapiResponseAdapter, + KibanaRequest, + KibanaResponse, + lifecycleResponseFactory, + LifecycleResponseFactory, +} from '../router'; enum ResultType { authenticated = 'authenticated', - redirected = 'redirected', - rejected = 'rejected', } -interface Authenticated extends AuthResultData { +interface Authenticated extends AuthResultParams { type: ResultType.authenticated; } -interface Redirected { - type: ResultType.redirected; - url: string; -} - -interface Rejected { - type: ResultType.rejected; - error: Error; - statusCode?: number; -} - -type AuthResult = Authenticated | Rejected | Redirected; +type AuthResult = Authenticated; const authResult = { - authenticated(data: Partial = {}): AuthResult { + authenticated(data: Partial = {}): AuthResult { return { type: ResultType.authenticated, - state: data.state || {}, - headers: data.headers || {}, + state: data.state, + requestHeaders: data.requestHeaders, + responseHeaders: data.responseHeaders, }; }, - redirected(url: string): AuthResult { - return { type: ResultType.redirected, url }; - }, - rejected(error: Error, options: { statusCode?: number } = {}): AuthResult { - return { type: ResultType.rejected, error, statusCode: options.statusCode }; - }, - isValid(candidate: any): candidate is AuthResult { - return ( - candidate && - (candidate.type === ResultType.authenticated || - candidate.type === ResultType.rejected || - candidate.type === ResultType.redirected) - ); - }, isAuthenticated(result: AuthResult): result is Authenticated { - return result.type === ResultType.authenticated; - }, - isRedirected(result: AuthResult): result is Redirected { - return result.type === ResultType.redirected; - }, - isRejected(result: AuthResult): result is Rejected { - return result.type === ResultType.rejected; + return result && result.type === ResultType.authenticated; }, }; @@ -82,21 +55,27 @@ const authResult = { * @public * */ -export type AuthHeaders = Record; +export type AuthHeaders = Record; /** * Result of an incoming request authentication. * @public * */ -export interface AuthResultData { +export interface AuthResultParams { /** * Data to associate with an incoming request. Any downstream plugin may get access to the data. */ - state: Record; + state?: Record; /** - * Auth specific headers to authenticate a user against Elasticsearch. + * Auth specific headers to attach to a request object. + * Used to perform a request to Elasticsearch on behalf of an authenticated user. */ - headers: AuthHeaders; + requestHeaders?: AuthHeaders; + /** + * Auth specific headers to attach to a response object. + * Used to send back authentication mechanism related headers to a client when needed. + */ + responseHeaders?: AuthHeaders; } /** @@ -105,52 +84,54 @@ export interface AuthResultData { */ export interface AuthToolkit { /** Authentication is successful with given credentials, allow request to pass through */ - authenticated: (data?: Partial) => AuthResult; - /** Authentication requires to interrupt request handling and redirect to a configured url */ - redirected: (url: string) => AuthResult; - /** Authentication is unsuccessful, fail the request with specified error. */ - rejected: (error: Error, options?: { statusCode?: number }) => AuthResult; + authenticated: (data?: AuthResultParams) => AuthResult; } const toolkit: AuthToolkit = { authenticated: authResult.authenticated, - redirected: authResult.redirected, - rejected: authResult.rejected, }; /** @public */ export type AuthenticationHandler = ( request: KibanaRequest, - t: AuthToolkit -) => AuthResult | Promise; + response: LifecycleResponseFactory, + toolkit: AuthToolkit +) => AuthResult | KibanaResponse | Promise; /** @public */ export function adoptToHapiAuthFormat( fn: AuthenticationHandler, - onSuccess: (req: Request, data: AuthResultData) => void = noop + log: Logger, + onSuccess: (req: Request, data: AuthResultParams) => void = () => undefined ) { return async function interceptAuth( - req: Request, - h: ResponseToolkit + request: Request, + responseToolkit: ResponseToolkit ): Promise { + const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); try { - const result = await fn(KibanaRequest.from(req, undefined, false), toolkit); - if (!authResult.isValid(result)) { - throw new Error( - `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.` - ); + const result = await fn( + KibanaRequest.from(request, undefined, false), + lifecycleResponseFactory, + toolkit + ); + if (result instanceof KibanaResponse) { + return hapiResponseAdapter.handle(result); } if (authResult.isAuthenticated(result)) { - onSuccess(req, { state: result.state, headers: result.headers }); - return h.authenticated({ credentials: result.state || {} }); - } - if (authResult.isRedirected(result)) { - return h.redirect(result.url).takeover(); + onSuccess(request, { + state: result.state, + requestHeaders: result.requestHeaders, + responseHeaders: result.responseHeaders, + }); + return responseToolkit.authenticated({ credentials: result.state || {} }); } - const { error, statusCode } = result; - return Boom.boomify(error, { statusCode }); + throw new Error( + `Unexpected result from Authenticate. Expected AuthResult or KibanaResponse, but given: ${result}.` + ); } catch (error) { - return Boom.internal(error.message, { statusCode: 500 }); + log.error(error); + return hapiResponseAdapter.toInternalError(); } }; } diff --git a/src/core/server/http/lifecycle/on_post_auth.test.ts b/src/core/server/http/lifecycle/on_post_auth.test.ts deleted file mode 100644 index 88e8fc91149b..000000000000 --- a/src/core/server/http/lifecycle/on_post_auth.test.ts +++ /dev/null @@ -1,95 +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 Boom from 'boom'; -import { adoptToHapiOnPostAuthFormat } from './on_post_auth'; -import { httpServerMock } from '../http_server.mocks'; - -describe('adoptToHapiOnPostAuthFormat', () => { - it('Should allow passing request to the next handler', async () => { - const continueSymbol = Symbol(); - const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => t.next()); - const result = await onPostAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit({ - ['continue']: continueSymbol, - }) - ); - - expect(result).toBe(continueSymbol); - }); - - it('Should support redirecting to specified url', async () => { - const redirectUrl = '/docs'; - const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => t.redirected(redirectUrl)); - const takeoverSymbol = {}; - const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); - const result = await onPostAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit({ - redirect: redirectMock, - }) - ); - - expect(redirectMock).toBeCalledWith(redirectUrl); - expect(result).toBe(takeoverSymbol); - }); - - it('Should support specifying statusCode and message for Boom error', async () => { - const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => { - return t.rejected(new Error('unexpected result'), { statusCode: 501 }); - }); - const result = (await onPostAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit() - )) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe('unexpected result'); - expect(result.output.statusCode).toBe(501); - }); - - it('Should return Boom.internal error if interceptor throws', async () => { - const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => { - throw new Error('unknown error'); - }); - const result = (await onPostAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit() - )) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe('unknown error'); - expect(result.output.statusCode).toBe(500); - }); - - it('Should return Boom.internal error if interceptor returns unexpected result', async () => { - const onPostAuth = adoptToHapiOnPostAuthFormat((req, toolkit) => undefined as any); - const result = (await onPostAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit() - )) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toMatchInlineSnapshot( - `"Unexpected result from OnPostAuth. Expected OnPostAuthResult, but given: undefined."` - ); - expect(result.output.statusCode).toBe(500); - }); -}); diff --git a/src/core/server/http/lifecycle/on_post_auth.ts b/src/core/server/http/lifecycle/on_post_auth.ts index 64d27bbe7c5c..a7f4eb57c269 100644 --- a/src/core/server/http/lifecycle/on_post_auth.ts +++ b/src/core/server/http/lifecycle/on_post_auth.ts @@ -17,59 +17,32 @@ * under the License. */ -import Boom from 'boom'; -import { Lifecycle, Request, ResponseToolkit } from 'hapi'; -import { KibanaRequest } from '../router'; +import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi'; +import { Logger } from '../../logging'; +import { + HapiResponseAdapter, + KibanaRequest, + KibanaResponse, + lifecycleResponseFactory, + LifecycleResponseFactory, +} from '../router'; enum ResultType { next = 'next', - redirected = 'redirected', - rejected = 'rejected', } interface Next { type: ResultType.next; } -interface Redirected { - type: ResultType.redirected; - url: string; -} - -interface Rejected { - type: ResultType.rejected; - error: Error; - statusCode?: number; -} - -type OnPostAuthResult = Next | Rejected | Redirected; +type OnPostAuthResult = Next; const postAuthResult = { next(): OnPostAuthResult { return { type: ResultType.next }; }, - redirected(url: string): OnPostAuthResult { - return { type: ResultType.redirected, url }; - }, - rejected(error: Error, options: { statusCode?: number } = {}): OnPostAuthResult { - return { type: ResultType.rejected, error, statusCode: options.statusCode }; - }, - isValid(candidate: any): candidate is OnPostAuthResult { - return ( - candidate && - (candidate.type === ResultType.next || - candidate.type === ResultType.rejected || - candidate.type === ResultType.redirected) - ); - }, isNext(result: OnPostAuthResult): result is Next { - return result.type === ResultType.next; - }, - isRedirected(result: OnPostAuthResult): result is Redirected { - return result.type === ResultType.redirected; - }, - isRejected(result: OnPostAuthResult): result is Rejected { - return result.type === ResultType.rejected; + return result && result.type === ResultType.next; }, }; @@ -80,51 +53,46 @@ const postAuthResult = { export interface OnPostAuthToolkit { /** To pass request to the next handler */ next: () => OnPostAuthResult; - /** To interrupt request handling and redirect to a configured url */ - redirected: (url: string) => OnPostAuthResult; - /** Fail the request with specified error. */ - rejected: (error: Error, options?: { statusCode?: number }) => OnPostAuthResult; } /** @public */ -export type OnPostAuthHandler = ( - request: KibanaRequest, - t: OnPostAuthToolkit -) => OnPostAuthResult | Promise; +export type OnPostAuthHandler = ( + request: KibanaRequest, + response: LifecycleResponseFactory, + toolkit: OnPostAuthToolkit +) => OnPostAuthResult | KibanaResponse | Promise; const toolkit: OnPostAuthToolkit = { next: postAuthResult.next, - redirected: postAuthResult.redirected, - rejected: postAuthResult.rejected, }; + /** * @public * Adopt custom request interceptor to Hapi lifecycle system. * @param fn - an extension point allowing to perform custom logic for * incoming HTTP requests. */ -export function adoptToHapiOnPostAuthFormat(fn: OnPostAuthHandler) { +export function adoptToHapiOnPostAuthFormat(fn: OnPostAuthHandler, log: Logger) { return async function interceptRequest( request: Request, - h: ResponseToolkit + responseToolkit: HapiResponseToolkit ): Promise { + const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); try { - const result = await fn(KibanaRequest.from(request), toolkit); - if (!postAuthResult.isValid(result)) { - throw new Error( - `Unexpected result from OnPostAuth. Expected OnPostAuthResult, but given: ${result}.` - ); + const result = await fn(KibanaRequest.from(request), lifecycleResponseFactory, toolkit); + if (result instanceof KibanaResponse) { + return hapiResponseAdapter.handle(result); } if (postAuthResult.isNext(result)) { - return h.continue; - } - if (postAuthResult.isRedirected(result)) { - return h.redirect(result.url).takeover(); + return responseToolkit.continue; } - const { error, statusCode } = result; - return Boom.boomify(error, { statusCode }); + + throw new Error( + `Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: ${result}.` + ); } catch (error) { - return Boom.internal(error.message, { statusCode: 500 }); + log.error(error); + return hapiResponseAdapter.toInternalError(); } }; } diff --git a/src/core/server/http/lifecycle/on_pre_auth.test.ts b/src/core/server/http/lifecycle/on_pre_auth.test.ts deleted file mode 100644 index bae7c9f16eb1..000000000000 --- a/src/core/server/http/lifecycle/on_pre_auth.test.ts +++ /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 Boom from 'boom'; -import { adoptToHapiOnPreAuthFormat } from './on_pre_auth'; -import { httpServerMock } from '../http_server.mocks'; - -describe('adoptToHapiOnPreAuthFormat', () => { - it('Should allow passing request to the next handler', async () => { - const continueSymbol = Symbol(); - const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => t.next()); - const result = await onPreAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit({ - ['continue']: continueSymbol, - }) - ); - - expect(result).toBe(continueSymbol); - }); - - it('Should support redirecting to specified url', async () => { - const redirectUrl = '/docs'; - const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => t.redirected(redirectUrl)); - const takeoverSymbol = {}; - const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); - const result = await onPreAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit({ - redirect: redirectMock, - }) - ); - - expect(redirectMock).toBeCalledWith(redirectUrl); - expect(result).toBe(takeoverSymbol); - }); - - it('Should support request forwarding to specified url', async () => { - const redirectUrl = '/docs'; - const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => - t.redirected(redirectUrl, { forward: true }) - ); - const continueSymbol = Symbol(); - const setUrl = jest.fn(); - const mockedRequest = httpServerMock.createRawRequest({ setUrl }); - const result = await onPreAuth( - mockedRequest, - httpServerMock.createRawResponseToolkit({ - ['continue']: continueSymbol, - }) - ); - - expect(setUrl).toBeCalledWith(redirectUrl); - expect(mockedRequest.raw.req.url).toBe(redirectUrl); - expect(result).toBe(continueSymbol); - }); - - it('Should support specifying statusCode and message for Boom error', async () => { - const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => { - return t.rejected(new Error('unexpected result'), { statusCode: 501 }); - }); - const result = (await onPreAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit() - )) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe('unexpected result'); - expect(result.output.statusCode).toBe(501); - }); - - it('Should return Boom.internal error if interceptor throws', async () => { - const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => { - throw new Error('unknown error'); - }); - const result = (await onPreAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit() - )) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe('unknown error'); - expect(result.output.statusCode).toBe(500); - }); - - it('Should return Boom.internal error if interceptor returns unexpected result', async () => { - const onPreAuth = adoptToHapiOnPreAuthFormat((req, toolkit) => undefined as any); - const result = (await onPreAuth( - httpServerMock.createRawRequest(), - httpServerMock.createRawResponseToolkit() - )) as Boom; - - expect(result).toBeInstanceOf(Boom); - expect(result.message).toMatchInlineSnapshot( - `"Unexpected result from OnPreAuth. Expected OnPreAuthResult, but given: undefined."` - ); - expect(result.output.statusCode).toBe(500); - }); -}); diff --git a/src/core/server/http/lifecycle/on_pre_auth.ts b/src/core/server/http/lifecycle/on_pre_auth.ts index 13b267b14866..ad204044ec34 100644 --- a/src/core/server/http/lifecycle/on_pre_auth.ts +++ b/src/core/server/http/lifecycle/on_pre_auth.ts @@ -17,60 +17,44 @@ * under the License. */ -import Boom from 'boom'; -import { Lifecycle, Request, ResponseToolkit } from 'hapi'; -import { KibanaRequest } from '../router'; +import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi'; +import { Logger } from '../../logging'; +import { + HapiResponseAdapter, + KibanaRequest, + KibanaResponse, + lifecycleResponseFactory, + LifecycleResponseFactory, +} from '../router'; enum ResultType { next = 'next', - redirected = 'redirected', - rejected = 'rejected', + rewriteUrl = 'rewriteUrl', } interface Next { type: ResultType.next; } -interface Redirected { - type: ResultType.redirected; +interface RewriteUrl { + type: ResultType.rewriteUrl; url: string; - forward?: boolean; } -interface Rejected { - type: ResultType.rejected; - error: Error; - statusCode?: number; -} - -type OnPreAuthResult = Next | Rejected | Redirected; +type OnPreAuthResult = Next | RewriteUrl; const preAuthResult = { next(): OnPreAuthResult { return { type: ResultType.next }; }, - redirected(url: string, options: { forward?: boolean } = {}): OnPreAuthResult { - return { type: ResultType.redirected, url, forward: options.forward }; - }, - rejected(error: Error, options: { statusCode?: number } = {}): OnPreAuthResult { - return { type: ResultType.rejected, error, statusCode: options.statusCode }; - }, - isValid(candidate: any): candidate is OnPreAuthResult { - return ( - candidate && - (candidate.type === ResultType.next || - candidate.type === ResultType.rejected || - candidate.type === ResultType.redirected) - ); + rewriteUrl(url: string): OnPreAuthResult { + return { type: ResultType.rewriteUrl, url }; }, isNext(result: OnPreAuthResult): result is Next { - return result.type === ResultType.next; - }, - isRedirected(result: OnPreAuthResult): result is Redirected { - return result.type === ResultType.redirected; + return result && result.type === ResultType.next; }, - isRejected(result: OnPreAuthResult): result is Rejected { - return result.type === ResultType.rejected; + isRewriteUrl(result: OnPreAuthResult): result is RewriteUrl { + return result && result.type === ResultType.rewriteUrl; }, }; @@ -81,26 +65,21 @@ const preAuthResult = { export interface OnPreAuthToolkit { /** To pass request to the next handler */ next: () => OnPreAuthResult; - /** - * To interrupt request handling and redirect to a configured url. - * If "options.forwarded" = true, request will be forwarded to another url right on the server. - * */ - redirected: (url: string, options?: { forward: boolean }) => OnPreAuthResult; - /** Fail the request with specified error. */ - rejected: (error: Error, options?: { statusCode?: number }) => OnPreAuthResult; + /** Rewrite requested resources url before is was authenticated and routed to a handler */ + rewriteUrl: (url: string) => OnPreAuthResult; } const toolkit: OnPreAuthToolkit = { next: preAuthResult.next, - redirected: preAuthResult.redirected, - rejected: preAuthResult.rejected, + rewriteUrl: preAuthResult.rewriteUrl, }; /** @public */ -export type OnPreAuthHandler = ( - request: KibanaRequest, - t: OnPreAuthToolkit -) => OnPreAuthResult | Promise; +export type OnPreAuthHandler = ( + request: KibanaRequest, + response: LifecycleResponseFactory, + toolkit: OnPreAuthToolkit +) => OnPreAuthResult | KibanaResponse | Promise; /** * @public @@ -108,38 +87,36 @@ export type OnPreAuthHandler = ( * @param fn - an extension point allowing to perform custom logic for * incoming HTTP requests. */ -export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler) { +export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler, log: Logger) { return async function interceptPreAuthRequest( request: Request, - h: ResponseToolkit + responseToolkit: HapiResponseToolkit ): Promise { - try { - const result = await fn(KibanaRequest.from(request), toolkit); + const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); - if (!preAuthResult.isValid(result)) { - throw new Error( - `Unexpected result from OnPreAuth. Expected OnPreAuthResult, but given: ${result}.` - ); + try { + const result = await fn(KibanaRequest.from(request), lifecycleResponseFactory, toolkit); + if (result instanceof KibanaResponse) { + return hapiResponseAdapter.handle(result); } + if (preAuthResult.isNext(result)) { - return h.continue; + return responseToolkit.continue; } - if (preAuthResult.isRedirected(result)) { - const { url, forward } = result; - if (forward) { - request.setUrl(url); - // We should update raw request as well since it can be proxied to the old platform - request.raw.req.url = url; - return h.continue; - } - return h.redirect(url).takeover(); + if (preAuthResult.isRewriteUrl(result)) { + const { url } = result; + request.setUrl(url); + // We should update raw request as well since it can be proxied to the old platform + request.raw.req.url = url; + return responseToolkit.continue; } - - const { error, statusCode } = result; - return Boom.boomify(error, { statusCode }); + throw new Error( + `Unexpected result from OnPreAuth. Expected OnPreAuthResult or KibanaResponse, but given: ${result}.` + ); } catch (error) { - return Boom.internal(error.message, { statusCode: 500 }); + log.error(error); + return hapiResponseAdapter.toInternalError(); } }; } diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index 956ef8cd2ebb..19eaee508199 100644 --- a/src/core/server/http/router/headers.ts +++ b/src/core/server/http/router/headers.ts @@ -16,11 +16,49 @@ * specific language governing permissions and limitations * under the License. */ +import { IncomingHttpHeaders } from 'http'; import { pick } from '../../../utils'; -/** @public */ -export type Headers = Record; +/** + * Creates a Union type of all known keys of a given interface. + * @example + * ```ts + * interface Person { + * name: string; + * age: number; + * [attributes: string]: string | number; + * } + * type PersonKnownKeys = KnownKeys; // "age" | "name" + * ``` + */ +type KnownKeys = { + [K in keyof T]: string extends K ? never : number extends K ? never : K; +} extends { [_ in keyof T]: infer U } + ? U + : never; + +/** + * Set of well-known HTTP headers. + * @public + */ +export type KnownHeaders = KnownKeys; + +/** + * Http request headers to read. + * @public + */ +export type Headers = { [header in KnownHeaders]?: string | string[] | undefined } & { + [header: string]: string | string[] | undefined; +}; + +/** + * Http response headers to set. + * @public + */ +export type ResponseHeaders = { [header in KnownHeaders]?: string | string[] } & { + [header: string]: string | string[]; +}; const normalizeHeaderField = (field: string) => field.trim().toLowerCase(); diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 0a1c402917e4..fa0dc5539764 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -17,7 +17,29 @@ * under the License. */ -export { Headers, filterHeaders } from './headers'; -export { Router } from './router'; -export { KibanaRequest, KibanaRequestRoute, ensureRawRequest, isRealRequest } from './request'; -export { RouteMethod, RouteConfigOptions } from './route'; +export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; +export { Router, RequestHandler } from './router'; +export { + KibanaRequest, + KibanaRequestRoute, + isRealRequest, + LegacyRequest, + ensureRawRequest, +} from './request'; +export { RouteMethod, RouteConfig, RouteConfigOptions } from './route'; +export { HapiResponseAdapter } from './response_adapter'; +export { + CustomHttpResponseOptions, + HttpResponseOptions, + HttpResponsePayload, + RedirectResponseOptions, + ResponseError, + ResponseErrorMeta, + KibanaResponse, + kibanaResponseFactory, + KibanaResponseFactory, + lifecycleResponseFactory, + LifecycleResponseFactory, +} from './response'; + +export { IKibanaSocket } from './socket'; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 7a1f4903b1cf..9e4d017f8db3 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -25,6 +25,7 @@ import { ObjectType, TypeOf } from '@kbn/config-schema'; import { deepFreeze, RecursiveReadonly } from '../../../utils'; import { Headers } from './headers'; import { RouteMethod, RouteSchemas, RouteConfigOptions } from './route'; +import { KibanaSocket, IKibanaSocket } from './socket'; const requestSymbol = Symbol('request'); @@ -38,16 +39,22 @@ export interface KibanaRequestRoute { options: Required; } +/** + * @deprecated + * `hapi` request object, supported during migration process only for backward compatibility. + * @public + */ +export interface LegacyRequest extends Request {} // eslint-disable-line @typescript-eslint/no-empty-interface + /** * Kibana specific abstraction for an incoming request. * @public - * */ + */ export class KibanaRequest { /** * Factory for creating requests. Validates the request before creating an * instance of a KibanaRequest. * @internal - * */ public static from

    ( req: Request, @@ -68,6 +75,7 @@ export class KibanaRequest { * Validates the different parts of a request based on the schemas defined for * the route. Builds up the actual params, query and body object that will be * received in the route handler. + * @internal */ private static validate

    ( req: Request, @@ -86,16 +94,25 @@ export class KibanaRequest { } const params = - routeSchemas.params === undefined ? {} : routeSchemas.params.validate(req.params); + routeSchemas.params === undefined + ? {} + : routeSchemas.params.validate(req.params, {}, 'request params'); - const query = routeSchemas.query === undefined ? {} : routeSchemas.query.validate(req.query); + const query = + routeSchemas.query === undefined + ? {} + : routeSchemas.query.validate(req.query, {}, 'request query'); - const body = routeSchemas.body === undefined ? {} : routeSchemas.body.validate(req.payload); + const body = + routeSchemas.body === undefined + ? {} + : routeSchemas.body.validate(req.payload, {}, 'request body'); return { query, params, body }; } - + /** a WHATWG URL standard object. */ public readonly url: Url; + /** matched route details */ public readonly route: RecursiveReadonly; /** * Readonly copy of incoming request headers. @@ -104,6 +121,8 @@ export class KibanaRequest { */ public readonly headers: Headers; + public readonly socket: IKibanaSocket; + /** @internal */ protected readonly [requestSymbol]: Request; @@ -126,6 +145,7 @@ export class KibanaRequest { }); this.route = deepFreeze(this.getRouteInfo()); + this.socket = new KibanaSocket(request.raw.req.socket); } private getRouteInfo() { @@ -145,14 +165,14 @@ export class KibanaRequest { * Returns underlying Hapi Request * @internal */ -export const ensureRawRequest = (request: KibanaRequest | Request) => +export const ensureRawRequest = (request: KibanaRequest | LegacyRequest) => isKibanaRequest(request) ? request[requestSymbol] : request; function isKibanaRequest(request: unknown): request is KibanaRequest { return request instanceof KibanaRequest; } -function isRequest(request: any): request is Request { +function isRequest(request: any): request is LegacyRequest { try { return request.raw.req && typeof request.raw.req === 'object'; } catch { @@ -164,6 +184,6 @@ function isRequest(request: any): request is Request { * Checks if an incoming request either KibanaRequest or Legacy.Request * @internal */ -export function isRealRequest(request: unknown): request is KibanaRequest | Request { +export function isRealRequest(request: unknown): request is KibanaRequest | LegacyRequest { return isKibanaRequest(request) || isRequest(request); } diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index 6e767aea0033..cd977e6b754a 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -16,19 +16,310 @@ * specific language governing permissions and limitations * under the License. */ +import { Stream } from 'stream'; +import { ResponseHeaders } from './headers'; -// TODO Needs _some_ work -export type StatusCode = 200 | 202 | 204 | 400; +/** + * Additional metadata to enhance error output or provide error details. + * @public + */ +export interface ResponseErrorMeta { + data?: Record; + errorCode?: string; + docLink?: string; +} +/** + * Error message and optional data send to the client in case of error. + * @public + */ +export type ResponseError = + | string + | Error + | { + message: string | Error; + meta?: ResponseErrorMeta; + }; -export class KibanaResponse { - constructor(readonly status: StatusCode, readonly payload?: T) {} +/** + * A response data object, expected to returned as a result of {@link RequestHandler} execution + * @internal + */ +export class KibanaResponse { + constructor( + readonly status: number, + readonly payload?: T, + readonly options: HttpResponseOptions = {} + ) {} } -export const responseFactory = { - accepted: (payload: T) => new KibanaResponse(202, payload), - badRequest: (err: T) => new KibanaResponse(400, err), - noContent: () => new KibanaResponse(204), - ok: (payload: T) => new KibanaResponse(200, payload), +/** + * HTTP response parameters + * @public + */ +export interface HttpResponseOptions { + /** HTTP Headers with additional information about response */ + headers?: ResponseHeaders; +} + +/** + * Data send to the client as a response payload. + * @public + */ +export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; + +/** + * HTTP response parameters for a response with adjustable status code. + * @public + */ +export interface CustomHttpResponseOptions extends HttpResponseOptions { + statusCode: number; +} + +/** + * HTTP response parameters for redirection response + * @public + */ +export type RedirectResponseOptions = HttpResponseOptions & { + headers: { + location: string; + }; +}; + +const successResponseFactory = { + /** + * The request has succeeded. + * Status code: `200`. + * @param payload - {@link HttpResponsePayload} payload to send to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + ok: (payload: HttpResponsePayload, options: HttpResponseOptions = {}) => + new KibanaResponse(200, payload, options), + + /** + * The request has been accepted for processing. + * Status code: `202`. + * @param payload - {@link HttpResponsePayload} payload to send to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + accepted: (payload?: HttpResponsePayload, options: HttpResponseOptions = {}) => + new KibanaResponse(202, payload, options), + + /** + * The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. + * Status code: `204`. + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + noContent: (options: HttpResponseOptions = {}) => new KibanaResponse(204, undefined, options), +}; + +const redirectionResponseFactory = { + /** + * Redirect to a different URI. + * Status code: `302`. + * @param payload - payload to send to the client + * @param options - {@link RedirectResponseOptions} configures HTTP response parameters. + * Expects `location` header to be set. + */ + redirected: (payload: HttpResponsePayload, options: RedirectResponseOptions) => + new KibanaResponse(302, payload, options), }; -export type ResponseFactory = typeof responseFactory; +const errorResponseFactory = { + /** + * The server cannot process the request due to something that is perceived to be a client error. + * Status code: `400`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + badRequest: (error: ResponseError = 'Bad Request', options: HttpResponseOptions = {}) => + new KibanaResponse(400, error, options), + + /** + * The request cannot be applied because it lacks valid authentication credentials for the target resource. + * Status code: `401`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + unauthorized: (error: ResponseError = 'Unauthorized', options: HttpResponseOptions = {}) => + new KibanaResponse(401, error, options), + + /** + * Server cannot grant access to a resource. + * Status code: `403`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + forbidden: (error: ResponseError = 'Forbidden', options: HttpResponseOptions = {}) => + new KibanaResponse(403, error, options), + + /** + * Server cannot find a current representation for the target resource. + * Status code: `404`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + notFound: (error: ResponseError = 'Not Found', options: HttpResponseOptions = {}) => + new KibanaResponse(404, error, options), + + /** + * The request could not be completed due to a conflict with the current state of the target resource. + * Status code: `409`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + conflict: (error: ResponseError = 'Conflict', options: HttpResponseOptions = {}) => + new KibanaResponse(409, error, options), + + // Server error + /** + * The server encountered an unexpected condition that prevented it from fulfilling the request. + * Status code: `500`. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link HttpResponseOptions} configures HTTP response parameters. + */ + internalError: (error: ResponseError = 'Internal Error', options: HttpResponseOptions = {}) => + new KibanaResponse(500, error, options), + + /** + * Creates an error response with defined status code and payload. + * @param error - {@link ResponseError} Error object containing message and other error details to pass to the client + * @param options - {@link CustomHttpResponseOptions} configures HTTP response parameters. + */ + customError: (error: ResponseError, options: CustomHttpResponseOptions) => { + if (!options || !options.statusCode) { + throw new Error(`options.statusCode is expected to be set. given options: ${options}`); + } + if (options.statusCode < 400 || options.statusCode >= 600) { + throw new Error( + `Unexpected Http status code. Expected from 400 to 599, but given: ${options.statusCode}` + ); + } + return new KibanaResponse(options.statusCode, error, options); + }, +}; +/** + * Set of helpers used to create `KibanaResponse` to form HTTP response on an incoming request. + * Should be returned as a result of {@link RequestHandler} execution. + * + * @example + * 1. Successful response. Supported types of response body are: + * - `undefined`, no content to send. + * - `string`, send text + * - `JSON`, send JSON object, HTTP server will throw if given object is not valid (has circular references, for example) + * - `Stream` send data stream + * - `Buffer` send binary stream + * ```js + * return response.ok(undefined); + * return response.ok('ack'); + * return response.ok({ id: '1' }); + * return response.ok(Buffer.from(...);); + * + * const stream = new Stream.PassThrough(); + * fs.createReadStream('./file').pipe(stream); + * return res.ok(stream); + * ``` + * HTTP headers are configurable via response factory parameter `options` {@link HttpResponseOptions}. + * + * ```js + * return response.ok({ id: '1' }, { + * headers: { + * 'content-type': 'application/json' + * } + * }); + * ``` + * 2. Redirection response. Redirection URL is configures via 'Location' header. + * ```js + * return response.redirected('The document has moved', { + * headers: { + * location: '/new-url', + * }, + * }); + * ``` + * 3. Error response. You may pass an error message to the client, where error message can be: + * - `string` send message text + * - `Error` send the message text of given Error object. + * - `{ message: string | Error, meta: {data: Record, ...} }` - send message text and attach additional error metadata. + * ```js + * return response.unauthorized('User has no access to the requested resource.', { + * headers: { + * 'WWW-Authenticate': 'challenge', + * } + * }) + * return response.badRequest(); + * return response.badRequest('validation error'); + * + * try { + * // ... + * } catch(error){ + * return response.badRequest(error); + * } + * + * return response.badRequest({ + * message: 'validation error', + * meta: { + * data: { + * requestBody: request.body, + * failedFields: validationResult + * }, + * } + * }); + * + * try { + * // ... + * } catch(error) { + * return response.badRequest({ + * message: error, + * meta: { + * data: { + * requestBody: request.body, + * }, + * } + * }); + * } + * + * ``` + * 4. Custom response. `ResponseFactory` may not cover your use case, so you can use the `custom` function to customize the response. + * ```js + * return response.custom('ok', { + * statusCode: 201, + * headers: { + * location: '/created-url' + * } + * }) + * ``` + * @public + */ +export const kibanaResponseFactory = { + ...successResponseFactory, + ...redirectionResponseFactory, + ...errorResponseFactory, + /** + * Creates a response with defined status code and payload. + * @param payload - {@link HttpResponsePayload} payload to send to the client + * @param options - {@link CustomHttpResponseOptions} configures HTTP response parameters. + */ + custom: (payload: HttpResponsePayload | ResponseError, options: CustomHttpResponseOptions) => { + if (!options || !options.statusCode) { + throw new Error(`options.statusCode is expected to be set. given options: ${options}`); + } + const { statusCode: code, ...rest } = options; + return new KibanaResponse(code, payload, rest); + }, +}; + +export const lifecycleResponseFactory = { + ...redirectionResponseFactory, + ...errorResponseFactory, +}; + +/** + * Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. + * @public + */ +export type KibanaResponseFactory = typeof kibanaResponseFactory; + +/** + * Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. + * @public + */ +export type LifecycleResponseFactory = typeof lifecycleResponseFactory; diff --git a/src/core/server/http/router/response_adapter.ts b/src/core/server/http/router/response_adapter.ts new file mode 100644 index 000000000000..89fdea1d67f6 --- /dev/null +++ b/src/core/server/http/router/response_adapter.ts @@ -0,0 +1,146 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ResponseObject as HapiResponseObject, ResponseToolkit as HapiResponseToolkit } from 'hapi'; +import typeDetect from 'type-detect'; +import Boom from 'boom'; + +import { HttpResponsePayload, KibanaResponse, ResponseError, ResponseErrorMeta } from './response'; + +declare module 'boom' { + interface Payload { + meta?: ResponseErrorMeta; + } +} + +function setHeaders(response: HapiResponseObject, headers: Record = {}) { + Object.entries(headers).forEach(([header, value]) => { + if (value !== undefined) { + // Hapi typings for header accept only strings, although string[] is a valid value + response.header(header, value as any); + } + }); + return response; +} + +const statusHelpers = { + isSuccess: (code: number) => code >= 100 && code < 300, + isRedirect: (code: number) => code >= 300 && code < 400, + isError: (code: number) => code >= 400 && code < 600, +}; + +export class HapiResponseAdapter { + constructor(private readonly responseToolkit: HapiResponseToolkit) {} + public toBadRequest(message: string) { + const error = Boom.badRequest(); + error.output.payload.message = message; + return error; + } + + public toInternalError() { + const error = new Boom('', { + statusCode: 500, + }); + + error.output.payload.message = 'An internal server error occurred.'; + + return error; + } + + public handle(kibanaResponse: KibanaResponse) { + if (!(kibanaResponse instanceof KibanaResponse)) { + throw new Error( + `Unexpected result from Route Handler. Expected KibanaResponse, but given: ${typeDetect( + kibanaResponse + )}.` + ); + } + + return this.toHapiResponse(kibanaResponse); + } + + private toHapiResponse(kibanaResponse: KibanaResponse) { + if (statusHelpers.isError(kibanaResponse.status)) { + return this.toError(kibanaResponse); + } + if (statusHelpers.isSuccess(kibanaResponse.status)) { + return this.toSuccess(kibanaResponse); + } + if (statusHelpers.isRedirect(kibanaResponse.status)) { + return this.toRedirect(kibanaResponse); + } + throw new Error( + `Unexpected Http status code. Expected from 100 to 599, but given: ${kibanaResponse.status}.` + ); + } + + private toSuccess(kibanaResponse: KibanaResponse) { + const response = this.responseToolkit + .response(kibanaResponse.payload) + .code(kibanaResponse.status); + setHeaders(response, kibanaResponse.options.headers); + return response; + } + + private toRedirect(kibanaResponse: KibanaResponse) { + const { headers } = kibanaResponse.options; + if (!headers || typeof headers.location !== 'string') { + throw new Error("expected 'location' header to be set"); + } + + const response = this.responseToolkit + .response(kibanaResponse.payload) + .redirect(headers.location) + .code(kibanaResponse.status) + .takeover(); + + setHeaders(response, kibanaResponse.options.headers); + return response; + } + + private toError(kibanaResponse: KibanaResponse) { + const { payload } = kibanaResponse; + // we use for BWC with Boom payload for error responses - {error: string, message: string, statusCode: string} + const error = new Boom('', { + statusCode: kibanaResponse.status, + }); + + error.output.payload.message = getErrorMessage(payload); + error.output.payload.meta = getErrorMeta(payload); + + const headers = kibanaResponse.options.headers; + if (headers) { + // Hapi typings for header accept only strings, although string[] is a valid value + error.output.headers = headers as any; + } + + return error; + } +} + +function getErrorMessage(payload?: ResponseError): string { + if (!payload) { + throw new Error('expected error message to be provided'); + } + if (typeof payload === 'string') return payload; + return getErrorMessage(payload.message); +} + +function getErrorMeta(payload?: ResponseError) { + return typeof payload === 'object' && 'meta' in payload ? payload.meta : undefined; +} diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index 42fb75db5f67..e80535601482 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -17,22 +17,22 @@ * under the License. */ -import { ObjectType, Schema } from '@kbn/config-schema'; +import { ObjectType } from '@kbn/config-schema'; /** * The set of common HTTP methods supported by Kibana routing. * @public - * */ + */ export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; /** - * Route specific configuration. + * Additional route options. * @public - * */ + */ export interface RouteConfigOptions { /** * A flag shows that authentication for a route: - * enabled when true - * disabled when false + * `enabled` when true + * `disabled` when false * * Enabled by default. */ @@ -44,34 +44,58 @@ export interface RouteConfigOptions { tags?: readonly string[]; } +/** + * Route specific configuration. + * @public + */ export interface RouteConfig

    { /** * The endpoint _within_ the router path to register the route. E.g. if the * router is registered at `/elasticsearch` and the route path is `/search`, * the full path for the route is `/elasticsearch/search`. + * Supports: + * - named path segments `path/{name}`. + * - optional path segments `path/{position?}`. + * - multi-segments `path/{coordinates*2}`. + * Segments are accessible within a handler function as `params` property of {@link KibanaRequest} object. + * To have read access to `params` you *must* specify validation schema with {@link RouteConfig.validate}. */ path: string; /** - * A function that will be called when setting up the route and that returns - * a schema that every request will be validated against. - * + * A schema created with `@kbn/config-schema` that every request will be validated against. + * You *must* specify a validation schema to be able to read: + * - url path segments + * - request query + * - request body * To opt out of validating the request, specify `false`. + * @example + * ```ts + * import { schema } from '@kbn/config-schema'; + * router.get({ + * path: 'path/{id}' + * validate: { + * params: schema.object({ + * id: schema.string(), + * }), + * query: schema.object({...}), + * body: schema.object({...}), + * }, + * }) + * ``` */ - validate: RouteValidateFactory | false; + validate: RouteSchemas | false; + /** + * Additional route options {@link RouteConfigOptions}. + */ options?: RouteConfigOptions; } -export type RouteValidateFactory< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType -> = (schema: Schema) => RouteSchemas; - /** * RouteSchemas contains the schemas for validating the different parts of a * request. + * @public */ export interface RouteSchemas

    { params?: P; diff --git a/src/core/server/http/router/router.test.ts b/src/core/server/http/router/router.test.ts new file mode 100644 index 000000000000..a0ab96257adc --- /dev/null +++ b/src/core/server/http/router/router.test.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Router } from './router'; +describe('Router', () => { + describe('Options', () => { + it('throws if validation for a route is not defined explicitly', () => { + const router = new Router('/foo'); + expect( + // we use 'any' because validate is a required field + () => router.get({ path: '/' } as any, (req, res) => res.ok({})) + ).toThrowErrorMatchingInlineSnapshot( + `"The [get] at [/] does not have a 'validate' specified. Use 'false' as the value if you want to bypass validation."` + ); + }); + it('throws if validation for a route is declared wrong', () => { + const router = new Router('/foo'); + expect(() => + router.get( + // we use 'any' because validate requires @kbn/config-schema usage + { path: '/', validate: { params: { validate: () => 'error' } } } as any, + (req, res) => res.ok({}) + ) + ).toThrowErrorMatchingInlineSnapshot( + `"Expected a valid schema declared with '@kbn/config-schema' package at key: [params]."` + ); + }); + }); +}); diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 9d3295a7f3bf..c175d5f8b431 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -17,28 +17,49 @@ * under the License. */ -import { ObjectType, schema, TypeOf } from '@kbn/config-schema'; +import { ObjectType, TypeOf, Type } from '@kbn/config-schema'; import { Request, ResponseObject, ResponseToolkit } from 'hapi'; +import Boom from 'boom'; +import { Logger } from '../../logging'; import { KibanaRequest } from './request'; -import { KibanaResponse, ResponseFactory, responseFactory } from './response'; +import { KibanaResponse, KibanaResponseFactory, kibanaResponseFactory } from './response'; import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; +import { HapiResponseAdapter } from './response_adapter'; -export interface RouterRoute { +interface RouterRoute { method: RouteMethod; path: string; options: RouteConfigOptions; - handler: (req: Request, responseToolkit: ResponseToolkit) => Promise; + handler: ( + request: Request, + responseToolkit: ResponseToolkit, + log: Logger + ) => Promise>; } -/** @public */ +/** + * Provides ability to declare a handler function for a particular path and HTTP request method. + * Each route can have only one handler functions, which is executed when the route is matched. + * + * @example + * ```ts + * const router = new Router('my-app'); + * // handler is called when 'my-app/path' resource is requested with `GET` method + * router.get({ path: '/path', validate: false }, (req, res) => res.ok({ content: 'ok' })); + * ``` + * + * @public + * */ export class Router { - public routes: Array> = []; + private routes: Array> = []; constructor(readonly path: string) {} /** - * Register a `GET` request with the router + * Register a route handler for `GET` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public get

    ( route: RouteConfig, @@ -47,8 +68,8 @@ export class Router { const { path, options = {} } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'get'); this.routes.push({ - handler: async (req, responseToolkit) => - await this.handle(routeSchemas, req, responseToolkit, handler), + handler: async (req, responseToolkit, log) => + await this.handle(routeSchemas, req, responseToolkit, handler, log), method: 'get', path, options, @@ -56,7 +77,9 @@ export class Router { } /** - * Register a `POST` request with the router + * Register a route handler for `POST` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public post

    ( route: RouteConfig, @@ -65,8 +88,8 @@ export class Router { const { path, options = {} } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'post'); this.routes.push({ - handler: async (req, responseToolkit) => - await this.handle(routeSchemas, req, responseToolkit, handler), + handler: async (req, responseToolkit, log) => + await this.handle(routeSchemas, req, responseToolkit, handler, log), method: 'post', path, options, @@ -74,7 +97,9 @@ export class Router { } /** - * Register a `PUT` request with the router + * Register a route handler for `PUT` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public put

    ( route: RouteConfig, @@ -83,8 +108,8 @@ export class Router { const { path, options = {} } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'put'); this.routes.push({ - handler: async (req, responseToolkit) => - await this.handle(routeSchemas, req, responseToolkit, handler), + handler: async (req, responseToolkit, log) => + await this.handle(routeSchemas, req, responseToolkit, handler, log), method: 'put', path, options, @@ -92,7 +117,9 @@ export class Router { } /** - * Register a `DELETE` request with the router + * Register a route handler for `DELETE` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public delete

    ( route: RouteConfig, @@ -101,8 +128,8 @@ export class Router { const { path, options = {} } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'delete'); this.routes.push({ - handler: async (req, responseToolkit) => - await this.handle(routeSchemas, req, responseToolkit, handler), + handler: async (req, responseToolkit, log) => + await this.handle(routeSchemas, req, responseToolkit, handler, log), method: 'delete', path, options, @@ -112,13 +139,14 @@ export class Router { /** * Returns all routes registered with the this router. * @returns List of registered routes. + * @internal */ public getRoutes() { return [...this.routes]; } /** - * Create the schemas for a route + * Create the validation schemas for a route * * @returns Route schemas if `validate` is specified on the route, otherwise * undefined. @@ -136,46 +164,78 @@ export class Router { ); } - return route.validate ? route.validate(schema) : undefined; + if (route.validate !== false) { + Object.entries(route.validate).forEach(([key, schema]) => { + if (!(schema instanceof Type)) { + throw new Error( + `Expected a valid schema declared with '@kbn/config-schema' package at key: [${key}].` + ); + } + }); + } + + return route.validate ? route.validate : undefined; } private async handle

    ( routeSchemas: RouteSchemas | undefined, request: Request, responseToolkit: ResponseToolkit, - handler: RequestHandler + handler: RequestHandler, + log: Logger ) { let kibanaRequest: KibanaRequest, TypeOf, TypeOf>; - + const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); try { kibanaRequest = KibanaRequest.from(request, routeSchemas); } catch (e) { - // TODO Handle failed validation - return responseToolkit.response({ error: e.message }).code(400); + return hapiResponseAdapter.toBadRequest(e.message); } try { - const kibanaResponse = await handler(kibanaRequest, responseFactory); - - let payload = null; - if (kibanaResponse.payload instanceof Error) { - // TODO Design an error format - payload = { error: kibanaResponse.payload.message }; - } else if (kibanaResponse.payload !== undefined) { - payload = kibanaResponse.payload; - } - - return responseToolkit.response(payload).code(kibanaResponse.status); + const kibanaResponse = await handler(kibanaRequest, kibanaResponseFactory); + return hapiResponseAdapter.handle(kibanaResponse); } catch (e) { - // TODO Handle `KibanaResponseError` - - // Otherwise we default to something along the lines of - return responseToolkit.response({ error: e.message }).code(500); + log.error(e); + return hapiResponseAdapter.toInternalError(); } } } +/** + * A function executed when route path matched requested resource path. + * Request handler is expected to return a result of one of {@link KibanaResponseFactory} functions. + * @param request {@link KibanaRequest} - object containing information about requested resource, + * such as path, method, headers, parameters, query, body, etc. + * @param response {@link KibanaResponseFactory} - a set of helper functions used to respond to a request. + * + * @example + * ```ts + * const router = new Router('my-app'); + * // creates a route handler for GET request on 'my-app/path/{id}' path + * router.get( + * { + * path: 'path/{id}', + * // defines a validation schema for a named segment of the route path + * validate: { + * params: schema.object({ + * id: schema.string(), + * }), + * }, + * }, + * // function to execute to create a responses + * async (request, response) => { + * const data = await findObject(request.params.id); + * // creates a command to respond with 'not found' error + * if (!data) return response.notFound(); + * // creates a command to send found data to the client + * return response.ok(data); + * } + * ); + * ``` + * @public + */ export type RequestHandler

    = ( - req: KibanaRequest, TypeOf, TypeOf>, - createResponse: ResponseFactory + request: KibanaRequest, TypeOf, TypeOf>, + response: KibanaResponseFactory ) => KibanaResponse | Promise>; diff --git a/src/core/server/http/router/socket.test.ts b/src/core/server/http/router/socket.test.ts new file mode 100644 index 000000000000..6bd903fd2f36 --- /dev/null +++ b/src/core/server/http/router/socket.test.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Socket } from 'net'; +import { DetailedPeerCertificate, TLSSocket } from 'tls'; +import { KibanaSocket } from './socket'; + +describe('KibanaSocket', () => { + describe('getPeerCertificate', () => { + it('returns null for net.Socket instance', () => { + const socket = new KibanaSocket(new Socket()); + + expect(socket.getPeerCertificate()).toBe(null); + }); + + it('delegates a call to tls.Socket instance', () => { + const tlsSocket = new TLSSocket(new Socket()); + const cert = { issuerCertificate: {} } as DetailedPeerCertificate; + const spy = jest.spyOn(tlsSocket, 'getPeerCertificate').mockImplementation(() => cert); + const socket = new KibanaSocket(tlsSocket); + const result = socket.getPeerCertificate(true); + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(true); + expect(result).toBe(cert); + }); + + it('returns null if tls.Socket getPeerCertificate returns null', () => { + const tlsSocket = new TLSSocket(new Socket()); + jest.spyOn(tlsSocket, 'getPeerCertificate').mockImplementation(() => null as any); + const socket = new KibanaSocket(tlsSocket); + + expect(socket.getPeerCertificate()).toBe(null); + }); + + it('returns null if tls.Socket getPeerCertificate returns empty object', () => { + const tlsSocket = new TLSSocket(new Socket()); + jest.spyOn(tlsSocket, 'getPeerCertificate').mockImplementation(() => ({} as any)); + const socket = new KibanaSocket(tlsSocket); + + expect(socket.getPeerCertificate()).toBe(null); + }); + }); +}); diff --git a/src/core/server/http/router/socket.ts b/src/core/server/http/router/socket.ts new file mode 100644 index 000000000000..2cdcd8f64100 --- /dev/null +++ b/src/core/server/http/router/socket.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Socket } from 'net'; +import { DetailedPeerCertificate, PeerCertificate, TLSSocket } from 'tls'; + +/** + * A tiny abstraction for TCP socket. + * @public + */ +export interface IKibanaSocket { + getPeerCertificate(detailed: true): DetailedPeerCertificate | null; + getPeerCertificate(detailed: false): PeerCertificate | null; + /** + * Returns an object representing the peer's certificate. + * The returned object has some properties corresponding to the field of the certificate. + * If detailed argument is true the full chain with issuer property will be returned, + * if false only the top certificate without issuer property. + * If the peer does not provide a certificate, it returns null. + * @param detailed - If true; the full chain with issuer property will be returned. + * @returns An object representing the peer's certificate. + */ + getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; +} + +export class KibanaSocket implements IKibanaSocket { + constructor(private readonly socket: Socket) {} + + getPeerCertificate(detailed: true): DetailedPeerCertificate | null; + getPeerCertificate(detailed: false): PeerCertificate | null; + getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; + + public getPeerCertificate(detailed?: boolean) { + if (this.socket instanceof TLSSocket) { + const peerCertificate = this.socket.getPeerCertificate(detailed); + + // If the peer does not provide a certificate, it returns null (if the socket has been destroyed) + // or an empty object, so we should check for both these cases. + if (peerCertificate && Object.keys(peerCertificate).length > 0) return peerCertificate; + } + return null; + } +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 4582f1362922..fc9eef7823a4 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -20,6 +20,10 @@ /** * The Kibana Core APIs for server-side plugins. * + * A plugin requires a `kibana.json` file at it's root directory that follows + * {@link PluginManifest | the manfiest schema} to define static plugin + * information required to load the plugin. + * * A plugin's `server/index` file must contain a named import, `plugin`, that * implements {@link PluginInitializer} which returns an object that implements * {@link Plugin}. @@ -42,10 +46,12 @@ import { ElasticsearchServiceSetup, } from './elasticsearch'; import { HttpServiceSetup, HttpServiceStart } from './http'; -import { PluginsServiceSetup, PluginsServiceStart } from './plugins'; +import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; +import { ContextSetup } from './context'; export { bootstrap } from './bootstrap'; -export { ConfigService } from './config'; +export { ConfigPath, ConfigService } from './config'; +export { CoreId } from './core_context'; export { CallAPIOptions, ClusterClient, @@ -56,25 +62,43 @@ export { ElasticsearchErrorHelpers, APICaller, FakeRequest, - LegacyRequest, } from './elasticsearch'; export { AuthenticationHandler, AuthHeaders, - AuthResultData, + AuthResultParams, + AuthStatus, AuthToolkit, + CustomHttpResponseOptions, GetAuthHeaders, + GetAuthState, + HttpResponseOptions, + HttpResponsePayload, + HttpServerSetup, + IKibanaSocket, + IsAuthenticated, KibanaRequest, KibanaRequestRoute, + LifecycleResponseFactory, + KnownHeaders, + LegacyRequest, OnPreAuthHandler, OnPreAuthToolkit, OnPostAuthHandler, OnPostAuthToolkit, + RedirectResponseOptions, + RequestHandler, + ResponseError, + ResponseErrorMeta, + kibanaResponseFactory, + KibanaResponseFactory, + RouteConfig, Router, RouteMethod, RouteConfigOptions, - SessionStorageFactory, SessionStorage, + SessionStorageCookieOptions, + SessionStorageFactory, } from './http'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; @@ -83,26 +107,35 @@ export { Plugin, PluginInitializer, PluginInitializerContext, + PluginManifest, PluginName, } from './plugins'; export { - SavedObject, - SavedObjectAttributes, - SavedObjectReference, - SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkResponse, SavedObjectsClient, - SavedObjectsClientContract, - SavedObjectsCreateOptions, + SavedObjectsClientProviderOptions, SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, + SavedObjectsCreateOptions, SavedObjectsErrorHelpers, - SavedObjectsFindOptions, + SavedObjectsExportOptions, SavedObjectsFindResponse, - SavedObjectsMigrationVersion, + SavedObjectsImportConflictError, + SavedObjectsImportError, + SavedObjectsImportMissingReferencesError, + SavedObjectsImportOptions, + SavedObjectsImportResponse, + SavedObjectsImportRetry, + SavedObjectsImportUnknownError, + SavedObjectsImportUnsupportedTypeError, + SavedObjectsMigrationLogger, + SavedObjectsRawDoc, + SavedObjectsResolveImportErrorsOptions, + SavedObjectsSchema, + SavedObjectsSerializer, SavedObjectsService, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, @@ -110,12 +143,26 @@ export { export { RecursiveReadonly } from '../utils'; +export { + SavedObject, + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsBaseOptions, + SavedObjectsClientContract, + SavedObjectsFindOptions, + SavedObjectsMigrationVersion, +} from './types'; + /** * Context passed to the plugins `setup` method. * * @public */ export interface CoreSetup { + context: { + createContextContainer: ContextSetup['createContextContainer']; + }; elasticsearch: { adminClient$: Observable; dataClient$: Observable; @@ -130,7 +177,6 @@ export interface CoreSetup { registerAuth: HttpServiceSetup['registerAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; basePath: HttpServiceSetup['basePath']; - createNewServer: HttpServiceSetup['createNewServer']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; }; } @@ -157,9 +203,11 @@ export interface InternalCoreStart { } export { + ContextSetup, HttpServiceSetup, HttpServiceStart, ElasticsearchServiceSetup, PluginsServiceSetup, PluginsServiceStart, + PluginOpaqueId, }; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index fa4d60520818..7da013dbe217 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -37,6 +37,7 @@ import { PluginsServiceSetup, PluginsServiceStart } from '../plugins/plugins_ser const MockKbnServer: jest.Mock = KbnServer as any; +let coreId: symbol; let env: Env; let config$: BehaviorSubject; let setupDeps: { @@ -60,6 +61,7 @@ const logger = loggingServiceMock.create(); let configService: ReturnType; beforeEach(() => { + coreId = Symbol(); env = Env.createDefault(getEnvOptions()); configService = configServiceMock.create(); @@ -112,7 +114,12 @@ afterEach(() => { describe('once LegacyService is set up with connection info', () => { test('creates legacy kbnServer and calls `listen`.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -136,7 +143,12 @@ describe('once LegacyService is set up with connection info', () => { test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -160,7 +172,12 @@ describe('once LegacyService is set up with connection info', () => { test('creates legacy kbnServer and closes it if `listen` fails.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed')); - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); @@ -172,7 +189,12 @@ describe('once LegacyService is set up with connection info', () => { test('throws if fails to retrieve initial config.', async () => { configService.getConfig$.mockReturnValue(throwError(new Error('something failed'))); - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); @@ -182,7 +204,12 @@ describe('once LegacyService is set up with connection info', () => { }); test('reconfigures logging configuration if new config is received.', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -197,7 +224,12 @@ describe('once LegacyService is set up with connection info', () => { }); test('logs error if re-configuring fails.', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -216,7 +248,12 @@ describe('once LegacyService is set up with connection info', () => { }); test('logs error if config service fails.', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -235,7 +272,7 @@ describe('once LegacyService is set up with connection info', () => { describe('once LegacyService is set up without connection info', () => { let legacyService: LegacyService; beforeEach(async () => { - legacyService = new LegacyService({ env, logger, configService: configService as any }); + legacyService = new LegacyService({ coreId, env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -277,6 +314,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { test('creates ClusterManager without base path proxy.', async () => { const devClusterLegacyService = new LegacyService({ + coreId, env: Env.createDefault( getEnvOptions({ cliArgs: { silent: true, basePath: false }, @@ -297,6 +335,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { test('creates ClusterManager with base path proxy.', async () => { const devClusterLegacyService = new LegacyService({ + coreId, env: Env.createDefault( getEnvOptions({ cliArgs: { quiet: true, basePath: true }, @@ -320,7 +359,12 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { }); test('Cannot start without setup phase', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Legacy service is not setup yet."` ); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 518221d2b9bd..da547e437648 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -21,6 +21,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart } from '.'; import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; import { httpServiceMock } from './http/http_service.mock'; +import { contextServiceMock } from './context/context_service.mock'; export { httpServerMock } from './http/http_server.mocks'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; @@ -41,6 +42,7 @@ export function pluginInitializerContextConfigMock(config: T) { function pluginInitializerContextMock(config: T) { const mock: PluginInitializerContext = { + opaqueId: Symbol(), logger: loggingServiceMock.create(), env: { mode: { @@ -57,6 +59,7 @@ function pluginInitializerContextMock(config: T) { function createCoreSetupMock() { const mock: MockedKeys = { + context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), }; diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index aedda9e3c92e..93c993a0fa37 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -22,7 +22,7 @@ import { resolve } from 'path'; import { coerce } from 'semver'; import { promisify } from 'util'; import { isConfigPath, PackageInfo } from '../../config'; -import { PluginManifest } from '../plugin'; +import { PluginManifest } from '../types'; import { PluginDiscoveryError } from './plugin_discovery_error'; const fsReadFileAsync = promisify(readFile); diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 6e174e03cfcb..224259bc121e 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -128,6 +128,7 @@ test('properly iterates through plugin search locations', async () => { .pipe(first()) .toPromise(); const { plugin$, error$ } = discover(new PluginsConfig(rawConfig, env), { + coreId: Symbol(), configService, env, logger, diff --git a/src/core/server/plugins/discovery/plugins_discovery.ts b/src/core/server/plugins/discovery/plugins_discovery.ts index f674444c921e..74e9dd709bb2 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.ts @@ -115,11 +115,13 @@ function createPlugin$(path: string, log: Logger, coreContext: CoreContext) { return from(parseManifest(path, coreContext.env.packageInfo)).pipe( map(manifest => { log.debug(`Successfully discovered plugin "${manifest.id}" at "${path}"`); - return new PluginWrapper( + const opaqueId = Symbol(manifest.id); + return new PluginWrapper({ path, manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); }), catchError(err => [err]) ); diff --git a/src/core/server/plugins/index.ts b/src/core/server/plugins/index.ts index c2e66cbb0bb7..c7ef213c8f18 100644 --- a/src/core/server/plugins/index.ts +++ b/src/core/server/plugins/index.ts @@ -21,12 +21,4 @@ export { PluginsService, PluginsServiceSetup, PluginsServiceStart } from './plug export { config } from './plugins_config'; /** @internal */ export { isNewPlatformPlugin } from './discovery'; -/** @internal */ -export { - DiscoveredPlugin, - DiscoveredPluginInternal, - Plugin, - PluginInitializer, - PluginName, -} from './plugin'; -export { PluginInitializerContext } from './plugin_context'; +export * from './types'; diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 0ce4ba266619..5c57a5fa2c8d 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -29,8 +29,10 @@ import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service import { httpServiceMock } from '../http/http_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; -import { PluginWrapper, PluginManifest } from './plugin'; +import { PluginWrapper } from './plugin'; +import { PluginManifest } from './types'; import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context'; +import { contextServiceMock } from '../context/context_service.mock'; const mockPluginInitializer = jest.fn(); const logger = loggingServiceMock.create(); @@ -63,16 +65,19 @@ function createPluginManifest(manifestProps: Partial = {}): Plug const configService = configServiceMock.create(); configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); +let coreId: symbol; let env: Env; let coreContext: CoreContext; const setupDeps = { + context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), }; beforeEach(() => { + coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); - coreContext = { env, logger, configService: configService as any }; + coreContext = { coreId, env, logger, configService: configService as any }; }); afterEach(() => { @@ -81,11 +86,13 @@ afterEach(() => { test('`constructor` correctly initializes plugin instance', () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'some-plugin-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'some-plugin-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(plugin.name).toBe('some-plugin-id'); expect(plugin.configPath).toBe('path'); @@ -96,11 +103,13 @@ test('`constructor` correctly initializes plugin instance', () => { test('`setup` fails if `plugin` initializer is not exported', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-without-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-without-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); await expect( plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) @@ -111,11 +120,13 @@ test('`setup` fails if `plugin` initializer is not exported', async () => { test('`setup` fails if plugin initializer is not a function', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-wrong-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-wrong-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); await expect( plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) @@ -126,11 +137,13 @@ test('`setup` fails if plugin initializer is not a function', async () => { test('`setup` fails if initializer does not return object', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); mockPluginInitializer.mockReturnValue(null); @@ -143,11 +156,13 @@ test('`setup` fails if initializer does not return object', async () => { test('`setup` fails if object returned from initializer does not define `setup` function', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); const mockPluginInstance = { run: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); @@ -161,8 +176,14 @@ test('`setup` fails if object returned from initializer does not define `setup` test('`setup` initializes plugin and calls appropriate lifecycle hook', async () => { const manifest = createPluginManifest(); - const initializerContext = createPluginInitializerContext(coreContext, manifest); - const plugin = new PluginWrapper('plugin-with-initializer-path', manifest, initializerContext); + const opaqueId = Symbol(); + const initializerContext = createPluginInitializerContext(coreContext, opaqueId, manifest); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', + manifest, + opaqueId, + initializerContext, + }); const mockPluginInstance = { setup: jest.fn().mockResolvedValue({ contract: 'yes' }) }; mockPluginInitializer.mockReturnValue(mockPluginInstance); @@ -180,11 +201,13 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async () test('`start` fails if setup is not called first', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'some-plugin-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'some-plugin-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Plugin \\"some-plugin-id\\" can't be started since it isn't set up."` @@ -193,11 +216,13 @@ test('`start` fails if setup is not called first', async () => { test('`start` calls plugin.start with context and dependencies', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; @@ -218,11 +243,13 @@ test('`start` calls plugin.start with context and dependencies', async () => { test('`stop` fails if plugin is not set up', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); @@ -235,11 +262,13 @@ test('`stop` fails if plugin is not set up', async () => { test('`stop` does nothing if plugin does not define `stop` function', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); mockPluginInitializer.mockReturnValue({ setup: jest.fn() }); await plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}); @@ -249,11 +278,13 @@ test('`stop` does nothing if plugin does not define `stop` function', async () = test('`stop` calls `stop` defined by the plugin instance', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); @@ -276,11 +307,13 @@ describe('#getConfigSchema()', () => { { virtual: true } ); const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-schema', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-schema', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(plugin.getConfigSchema()).toBe(pluginSchema); }); @@ -288,21 +321,25 @@ describe('#getConfigSchema()', () => { it('returns null if config definition not specified', () => { jest.doMock('plugin-with-no-definition/server', () => ({}), { virtual: true }); const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-no-definition', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-no-definition', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(plugin.getConfigSchema()).toBe(null); }); it('returns null for plugins without a server part', () => { const manifest = createPluginManifest({ server: false }); - const plugin = new PluginWrapper( - 'plugin-with-no-definition', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-no-definition', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(plugin.getConfigSchema()).toBe(null); }); @@ -319,11 +356,13 @@ describe('#getConfigSchema()', () => { { virtual: true } ); const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-invalid-schema', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-invalid-schema', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(() => plugin.getConfigSchema()).toThrowErrorMatchingInlineSnapshot( `"Configuration schema expected to be an instance of Type"` ); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 289f6f7cda7a..0101862ad32c 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -22,143 +22,17 @@ import typeDetect from 'type-detect'; import { Type } from '@kbn/config-schema'; -import { ConfigPath } from '../config'; import { Logger } from '../logging'; -import { PluginInitializerContext } from './plugin_context'; +import { + Plugin, + PluginInitializerContext, + PluginManifest, + PluginConfigSchema, + PluginInitializer, + PluginOpaqueId, +} from './types'; import { CoreSetup, CoreStart } from '..'; -export type PluginConfigSchema = Type | null; - -/** - * Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays - * that use it as a key or value more obvious. - * - * @public - */ -export type PluginName = string; - -/** - * Describes the set of required and optional properties plugin can define in its - * mandatory JSON manifest file. - * @internal - */ -export interface PluginManifest { - /** - * Identifier of the plugin. - */ - readonly id: PluginName; - - /** - * Version of the plugin. - */ - readonly version: string; - - /** - * The version of Kibana the plugin is compatible with, defaults to "version". - */ - readonly kibanaVersion: string; - - /** - * Root configuration path used by the plugin, defaults to "id". - */ - readonly configPath: ConfigPath; - - /** - * An optional list of the other plugins that **must be** installed and enabled - * for this plugin to function properly. - */ - readonly requiredPlugins: readonly PluginName[]; - - /** - * An optional list of the other plugins that if installed and enabled **may be** - * leveraged by this plugin for some additional functionality but otherwise are - * not required for this plugin to work properly. - */ - readonly optionalPlugins: readonly PluginName[]; - - /** - * Specifies whether plugin includes some client/browser specific functionality - * that should be included into client bundle via `public/ui_plugin.js` file. - */ - readonly ui: boolean; - - /** - * Specifies whether plugin includes some server-side specific functionality. - */ - readonly server: boolean; -} - -/** - * Small container object used to expose information about discovered plugins that may - * or may not have been started. - * @public - */ -export interface DiscoveredPlugin { - /** - * Identifier of the plugin. - */ - readonly id: PluginName; - - /** - * Root configuration path used by the plugin, defaults to "id". - */ - readonly configPath: ConfigPath; - - /** - * An optional list of the other plugins that **must be** installed and enabled - * for this plugin to function properly. - */ - readonly requiredPlugins: readonly PluginName[]; - - /** - * An optional list of the other plugins that if installed and enabled **may be** - * leveraged by this plugin for some additional functionality but otherwise are - * not required for this plugin to work properly. - */ - readonly optionalPlugins: readonly PluginName[]; -} - -/** - * An extended `DiscoveredPlugin` that exposes more sensitive information. Should never - * be exposed to client-side code. - * @internal - */ -export interface DiscoveredPluginInternal extends DiscoveredPlugin { - /** - * Path on the filesystem where plugin was loaded from. - */ - readonly path: string; -} - -/** - * The interface that should be returned by a `PluginInitializer`. - * - * @public - */ -export interface Plugin< - TSetup = void, - TStart = void, - TPluginsSetup extends object = object, - TPluginsStart extends object = object -> { - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; - stop?(): void; -} - -/** - * The `plugin` export at the root of a plugin's `server` directory should conform - * to this interface. - * - * @public - */ -export type PluginInitializer< - TSetup, - TStart, - TPluginsSetup extends object = object, - TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; - /** * Lightweight wrapper around discovered plugin that is responsible for instantiating * plugin and dispatching proper context and dependencies into plugin's lifecycle hooks. @@ -171,6 +45,9 @@ export class PluginWrapper< TPluginsSetup extends object = object, TPluginsStart extends object = object > { + public readonly path: string; + public readonly manifest: PluginManifest; + public readonly opaqueId: PluginOpaqueId; public readonly name: PluginManifest['id']; public readonly configPath: PluginManifest['configPath']; public readonly requiredPlugins: PluginManifest['requiredPlugins']; @@ -179,21 +56,29 @@ export class PluginWrapper< public readonly includesUiPlugin: PluginManifest['ui']; private readonly log: Logger; + private readonly initializerContext: PluginInitializerContext; private instance?: Plugin; constructor( - public readonly path: string, - public readonly manifest: PluginManifest, - private readonly initializerContext: PluginInitializerContext + readonly params: { + readonly path: string; + readonly manifest: PluginManifest; + readonly opaqueId: PluginOpaqueId; + readonly initializerContext: PluginInitializerContext; + } ) { - this.log = initializerContext.logger.get(); - this.name = manifest.id; - this.configPath = manifest.configPath; - this.requiredPlugins = manifest.requiredPlugins; - this.optionalPlugins = manifest.optionalPlugins; - this.includesServerPlugin = manifest.server; - this.includesUiPlugin = manifest.ui; + this.path = params.path; + this.manifest = params.manifest; + this.opaqueId = params.opaqueId; + this.initializerContext = params.initializerContext; + this.log = params.initializerContext.logger.get(); + this.name = params.manifest.id; + this.configPath = params.manifest.configPath; + this.requiredPlugins = params.manifest.requiredPlugins; + this.optionalPlugins = params.manifest.optionalPlugins; + this.includesServerPlugin = params.manifest.server; + this.includesUiPlugin = params.manifest.ui; } /** diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index fcc8a26f51b4..01c9aa2f8d1a 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -17,28 +17,12 @@ * under the License. */ -import { Observable } from 'rxjs'; -import { EnvironmentMode } from '../config'; import { CoreContext } from '../core_context'; -import { LoggerFactory } from '../logging'; -import { PluginWrapper, PluginManifest } from './plugin'; +import { PluginWrapper } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; +import { PluginInitializerContext, PluginManifest, PluginOpaqueId } from './types'; import { CoreSetup, CoreStart } from '..'; -/** - * Context that's available to plugins during initialization stage. - * - * @public - */ -export interface PluginInitializerContext { - env: { mode: EnvironmentMode }; - logger: LoggerFactory; - config: { - create: () => Observable; - createIfExists: () => Observable; - }; -} - /** * This returns a facade for `CoreContext` that will be exposed to the plugin initializer. * This facade should be safe to use across entire plugin lifespan. @@ -54,9 +38,12 @@ export interface PluginInitializerContext { */ export function createPluginInitializerContext( coreContext: CoreContext, + opaqueId: PluginOpaqueId, pluginManifest: PluginManifest ): PluginInitializerContext { return { + opaqueId, + /** * Environment information that is safe to expose to plugins and may be beneficial for them. */ @@ -112,6 +99,9 @@ export function createPluginSetupContext( plugin: PluginWrapper ): CoreSetup { return { + context: { + createContextContainer: deps.context.createContextContainer, + }, elasticsearch: { adminClient$: deps.elasticsearch.adminClient$, dataClient$: deps.elasticsearch.dataClient$, @@ -123,7 +113,6 @@ export function createPluginSetupContext( registerAuth: deps.http.registerAuth, registerOnPostAuth: deps.http.registerOnPostAuth, basePath: deps.http.basePath, - createNewServer: deps.http.createNewServer, isTlsEnabled: deps.http.isTlsEnabled, }, }; diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index c9045ad04e1e..c8b6bed044fd 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -22,6 +22,7 @@ import { PluginsService } from './plugins_service'; type ServiceContract = PublicMethodsOf; const createServiceMock = () => { const mocked: jest.Mocked = { + discover: jest.fn(), setup: jest.fn(), start: jest.fn(), stop: jest.fn(), diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index fbb37f40362b..fdbb5efbfafe 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -33,14 +33,17 @@ import { PluginWrapper } from './plugin'; import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; +import { contextServiceMock } from '../context/context_service.mock'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; let pluginsService: PluginsService; let configService: ConfigService; +let coreId: symbol; let env: Env; let mockPluginSystem: jest.Mocked; const setupDeps = { + context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), }; @@ -63,6 +66,7 @@ beforeEach(async () => { }, }; + coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); configService = new ConfigService( @@ -71,7 +75,7 @@ beforeEach(async () => { logger ); await configService.setSchema(config.path, config.schema); - pluginsService = new PluginsService({ env, logger, configService }); + pluginsService = new PluginsService({ coreId, env, logger, configService }); [mockPluginSystem] = MockPluginsSystem.mock.instances as any; }); @@ -80,13 +84,13 @@ afterEach(() => { jest.clearAllMocks(); }); -test('`setup` throws if plugin has an invalid manifest', async () => { +test('`discover` throws if plugin has an invalid manifest', async () => { mockDiscover.mockReturnValue({ error$: from([PluginDiscoveryError.invalidManifest('path-1', new Error('Invalid JSON'))]), plugin$: from([]), }); - await expect(pluginsService.setup(setupDeps)).rejects.toMatchInlineSnapshot(` + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Invalid JSON (invalid-manifest, path-1)] `); @@ -99,7 +103,7 @@ Array [ `); }); -test('`setup` throws if plugin required Kibana version is incompatible with the current version', async () => { +test('`discover` throws if plugin required Kibana version is incompatible with the current version', async () => { mockDiscover.mockReturnValue({ error$: from([ PluginDiscoveryError.incompatibleVersion('path-3', new Error('Incompatible version')), @@ -107,7 +111,7 @@ test('`setup` throws if plugin required Kibana version is incompatible with the plugin$: from([]), }); - await expect(pluginsService.setup(setupDeps)).rejects.toMatchInlineSnapshot(` + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Incompatible version (incompatible-version, path-3)] `); @@ -120,13 +124,13 @@ Array [ `); }); -test('`setup` throws if discovered plugins with conflicting names', async () => { +test('`discover` throws if discovered plugins with conflicting names', async () => { mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - new PluginWrapper( - 'path-4', - { + new PluginWrapper({ + path: 'path-4', + manifest: { id: 'conflicting-id', version: 'some-version', configPath: 'path', @@ -136,11 +140,12 @@ test('`setup` throws if discovered plugins with conflicting names', async () => server: true, ui: true, }, - { logger } as any - ), - new PluginWrapper( - 'path-5', - { + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), + new PluginWrapper({ + path: 'path-5', + manifest: { id: 'conflicting-id', version: 'some-other-version', configPath: ['plugin', 'path'], @@ -150,12 +155,13 @@ test('`setup` throws if discovered plugins with conflicting names', async () => server: true, ui: false, }, - { logger } as any - ), + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), ]), }); - await expect(pluginsService.setup(setupDeps)).rejects.toMatchInlineSnapshot( + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot( `[Error: Plugin with id "conflicting-id" is already registered!]` ); @@ -163,7 +169,7 @@ test('`setup` throws if discovered plugins with conflicting names', async () => expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled(); }); -test('`setup` properly detects plugins that should be disabled.', async () => { +test('`discover` properly detects plugins that should be disabled.', async () => { jest .spyOn(configService, 'isEnabledAtPath') .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); @@ -174,9 +180,9 @@ test('`setup` properly detects plugins that should be disabled.', async () => { mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - new PluginWrapper( - 'path-1', - { + new PluginWrapper({ + path: 'path-1', + manifest: { id: 'explicitly-disabled-plugin', version: 'some-version', configPath: 'path-1-disabled', @@ -186,11 +192,12 @@ test('`setup` properly detects plugins that should be disabled.', async () => { server: true, ui: true, }, - { logger } as any - ), - new PluginWrapper( - 'path-2', - { + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), + new PluginWrapper({ + path: 'path-2', + manifest: { id: 'plugin-with-missing-required-deps', version: 'some-version', configPath: 'path-2', @@ -200,11 +207,12 @@ test('`setup` properly detects plugins that should be disabled.', async () => { server: true, ui: true, }, - { logger } as any - ), - new PluginWrapper( - 'path-3', - { + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), + new PluginWrapper({ + path: 'path-3', + manifest: { id: 'plugin-with-disabled-transitive-dep', version: 'some-version', configPath: 'path-3', @@ -214,11 +222,12 @@ test('`setup` properly detects plugins that should be disabled.', async () => { server: true, ui: true, }, - { logger } as any - ), - new PluginWrapper( - 'path-4', - { + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), + new PluginWrapper({ + path: 'path-4', + manifest: { id: 'another-explicitly-disabled-plugin', version: 'some-version', configPath: 'path-4-disabled', @@ -228,16 +237,18 @@ test('`setup` properly detects plugins that should be disabled.', async () => { server: true, ui: true, }, - { logger } as any - ), + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), ]), }); - const start = await pluginsService.setup(setupDeps); + await pluginsService.discover(); + const setup = await pluginsService.setup(setupDeps); - expect(start.contracts).toBeInstanceOf(Map); - expect(start.uiPlugins.public).toBeInstanceOf(Map); - expect(start.uiPlugins.internal).toBeInstanceOf(Map); + expect(setup.contracts).toBeInstanceOf(Map); + expect(setup.uiPlugins.public).toBeInstanceOf(Map); + expect(setup.uiPlugins.internal).toBeInstanceOf(Map); expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); @@ -260,10 +271,10 @@ Array [ `); }); -test('`setup` properly invokes `discover` and ignores non-critical errors.', async () => { - const firstPlugin = new PluginWrapper( - 'path-1', - { +test('`discover` properly invokes plugin discovery and ignores non-critical errors.', async () => { + const firstPlugin = new PluginWrapper({ + path: 'path-1', + manifest: { id: 'some-id', version: 'some-version', configPath: 'path', @@ -273,12 +284,13 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy server: true, ui: true, }, - { logger } as any - ); + opaqueId: Symbol(), + initializerContext: { logger } as any, + }); - const secondPlugin = new PluginWrapper( - 'path-2', - { + const secondPlugin = new PluginWrapper({ + path: 'path-2', + manifest: { id: 'some-other-id', version: 'some-other-version', configPath: ['plugin', 'path'], @@ -288,8 +300,9 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy server: true, ui: false, }, - { logger } as any - ); + opaqueId: Symbol(), + initializerContext: { logger } as any, + }); mockDiscover.mockReturnValue({ error$: from([ @@ -300,15 +313,7 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy plugin$: from([firstPlugin, secondPlugin]), }); - const contracts = new Map(); - const discoveredPlugins = { public: new Map(), internal: new Map() }; - mockPluginSystem.setupPlugins.mockResolvedValue(contracts); - mockPluginSystem.uiPlugins.mockReturnValue(discoveredPlugins); - - const setup = await pluginsService.setup(setupDeps); - - expect(setup.contracts).toBe(contracts); - expect(setup.uiPlugins).toBe(discoveredPlugins); + await pluginsService.discover(); expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); @@ -325,7 +330,7 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy resolve(process.cwd(), '..', 'kibana-extra'), ], }, - { env, logger, configService } + { coreId, env, logger, configService } ); const logs = loggingServiceMock.collect(logger); @@ -338,7 +343,7 @@ test('`stop` stops plugins system', async () => { expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); }); -test('`setup` registers plugin config schema in config service', async () => { +test('`discover` registers plugin config schema in config service', async () => { const configSchema = schema.string(); jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); jest.doMock( @@ -355,9 +360,9 @@ test('`setup` registers plugin config schema in config service', async () => { mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - new PluginWrapper( - 'path-with-schema', - { + new PluginWrapper({ + path: 'path-with-schema', + manifest: { id: 'some-id', version: 'some-version', configPath: 'path', @@ -367,10 +372,11 @@ test('`setup` registers plugin config schema in config service', async () => { server: true, ui: true, }, - { logger } as any - ), + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), ]), }); - await pluginsService.setup(setupDeps); + await pluginsService.discover(); expect(configService.setSchema).toBeCalledWith('path', configSchema); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 95d3f26fff91..0fe20e7e59c3 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -25,9 +25,11 @@ import { ElasticsearchServiceSetup } from '../elasticsearch/elasticsearch_servic import { HttpServiceSetup } from '../http/http_service'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } from './plugin'; +import { PluginWrapper } from './plugin'; +import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; +import { ContextSetup } from '../context'; /** @public */ export interface PluginsServiceSetup { @@ -45,6 +47,7 @@ export interface PluginsServiceStart { /** @internal */ export interface PluginsServiceSetupDeps { + context: ContextSetup; elasticsearch: ElasticsearchServiceSetup; http: HttpServiceSetup; } @@ -66,8 +69,8 @@ export class PluginsService implements CoreService new PluginsConfig(rawConfig, coreContext.env))); } - public async setup(deps: PluginsServiceSetupDeps) { - this.log.debug('Setting up plugins service'); + public async discover() { + this.log.debug('Discovering plugins'); const config = await this.config$.pipe(first()).toPromise(); @@ -75,6 +78,15 @@ export class PluginsService implements CoreService { env = Env.createDefault(getEnvOptions()); - coreContext = { env, logger, configService: configService as any }; + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; pluginsSystem = new PluginsSystem(coreContext); }); @@ -87,6 +91,27 @@ test('can be setup even without plugins', async () => { expect(pluginsSetup.size).toBe(0); }); +test('getPluginDependencies returns dependency tree of symbols', () => { + pluginsSystem.addPlugin(createPlugin('plugin-a', { required: ['no-dep'] })); + pluginsSystem.addPlugin( + createPlugin('plugin-b', { required: ['plugin-a'], optional: ['no-dep', 'other'] }) + ); + pluginsSystem.addPlugin(createPlugin('no-dep')); + + expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(` + Map { + Symbol(plugin-a) => Array [ + Symbol(no-dep), + ], + Symbol(plugin-b) => Array [ + Symbol(plugin-a), + Symbol(no-dep), + ], + Symbol(no-dep) => Array [], + } + `); +}); + test('`setupPlugins` throws plugin has missing required dependency', async () => { pluginsSystem.addPlugin(createPlugin('some-id', { required: ['missing-dep'] })); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 37eab8226af7..1f797525ba14 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -21,7 +21,8 @@ import { pick } from 'lodash'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } from './plugin'; +import { PluginWrapper } from './plugin'; +import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName, PluginOpaqueId } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; @@ -40,6 +41,25 @@ export class PluginsSystem { this.plugins.set(plugin.name, plugin); } + /** + * @returns a ReadonlyMap of each plugin and an Array of its available dependencies + * @internal + */ + public getPluginDependencies(): ReadonlyMap { + // Return dependency map of opaque ids + return new Map( + [...this.plugins].map(([name, plugin]) => [ + plugin.opaqueId, + [ + ...new Set([ + ...plugin.requiredPlugins, + ...plugin.optionalPlugins.filter(optPlugin => this.plugins.has(optPlugin)), + ]), + ].map(depId => this.plugins.get(depId)!.opaqueId), + ]) + ); + } + public async setupPlugins(deps: PluginsServiceSetupDeps) { const contracts = new Map(); if (this.plugins.size === 0) { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts new file mode 100644 index 000000000000..4b66c9fb65c1 --- /dev/null +++ b/src/core/server/plugins/types.ts @@ -0,0 +1,181 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Type } from '@kbn/config-schema'; + +import { ConfigPath, EnvironmentMode } from '../config'; +import { LoggerFactory } from '../logging'; +import { CoreSetup, CoreStart } from '..'; + +export type PluginConfigSchema = Type | null; + +/** + * Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays + * that use it as a key or value more obvious. + * + * @public + */ +export type PluginName = string; + +/** @public */ +export type PluginOpaqueId = symbol; + +/** + * Describes the set of required and optional properties plugin can define in its + * mandatory JSON manifest file. + * + * @remarks + * Should never be used in code outside of Core but is exported for + * documentation purposes. + * + * @public + */ +export interface PluginManifest { + /** + * Identifier of the plugin. + */ + readonly id: PluginName; + + /** + * Version of the plugin. + */ + readonly version: string; + + /** + * The version of Kibana the plugin is compatible with, defaults to "version". + */ + readonly kibanaVersion: string; + + /** + * Root {@link ConfigPath | configuration path} used by the plugin, defaults + * to "id". + */ + readonly configPath: ConfigPath; + + /** + * An optional list of the other plugins that **must be** installed and enabled + * for this plugin to function properly. + */ + readonly requiredPlugins: readonly PluginName[]; + + /** + * An optional list of the other plugins that if installed and enabled **may be** + * leveraged by this plugin for some additional functionality but otherwise are + * not required for this plugin to work properly. + */ + readonly optionalPlugins: readonly PluginName[]; + + /** + * Specifies whether plugin includes some client/browser specific functionality + * that should be included into client bundle via `public/ui_plugin.js` file. + */ + readonly ui: boolean; + + /** + * Specifies whether plugin includes some server-side specific functionality. + */ + readonly server: boolean; +} + +/** + * Small container object used to expose information about discovered plugins that may + * or may not have been started. + * @public + */ +export interface DiscoveredPlugin { + /** + * Identifier of the plugin. + */ + readonly id: PluginName; + + /** + * Root configuration path used by the plugin, defaults to "id". + */ + readonly configPath: ConfigPath; + + /** + * An optional list of the other plugins that **must be** installed and enabled + * for this plugin to function properly. + */ + readonly requiredPlugins: readonly PluginName[]; + + /** + * An optional list of the other plugins that if installed and enabled **may be** + * leveraged by this plugin for some additional functionality but otherwise are + * not required for this plugin to work properly. + */ + readonly optionalPlugins: readonly PluginName[]; +} + +/** + * An extended `DiscoveredPlugin` that exposes more sensitive information. Should never + * be exposed to client-side code. + * @internal + */ +export interface DiscoveredPluginInternal extends DiscoveredPlugin { + /** + * Path on the filesystem where plugin was loaded from. + */ + readonly path: string; +} + +/** + * The interface that should be returned by a `PluginInitializer`. + * + * @public + */ +export interface Plugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + stop?(): void; +} + +/** + * Context that's available to plugins during initialization stage. + * + * @public + */ +export interface PluginInitializerContext { + opaqueId: PluginOpaqueId; + env: { mode: EnvironmentMode }; + logger: LoggerFactory; + config: { + create: () => Observable; + createIfExists: () => Observable; + }; +} + +/** + * The `plugin` export at the root of a plugin's `server` directory should conform + * to this interface. + * + * @public + */ +export type PluginInitializer< + TSetup, + TStart, + TPluginsSetup extends object = object, + TPluginsStart extends object = object +> = (core: PluginInitializerContext) => Plugin; diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts index 385de5b14565..618a83b0fb68 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -74,50 +74,134 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Object { - "perPage": 500, - "sortField": "_id", - "sortOrder": "asc", - "type": Array [ - "index-pattern", - "search", + [MockFunction] { + "calls": Array [ + Array [ + Object { + "namespace": undefined, + "perPage": 500, + "sortField": "_id", + "sortOrder": "asc", + "type": Array [ + "index-pattern", + "search", + ], + }, + ], ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + test('exports from the provided namespace when present', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await getSortedObjectsForExport({ + savedObjectsClient, + exportSizeLimit: 500, + types: ['index-pattern', 'search'], + namespace: 'foo', + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); + expect(savedObjectsClient.find).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Object { + "namespace": "foo", + "perPage": 500, + "sortField": "_id", + "sortOrder": "asc", + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('export selected types throws error when exceeding exportSizeLimit', async () => { @@ -195,51 +279,54 @@ Array [ }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, -] -`); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ Array [ Object { + "attributes": Object {}, "id": "1", + "references": Array [], "type": "index-pattern", }, Object { + "attributes": Object {}, "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], "type": "search", }, - ], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + ] + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + Object { + "id": "2", + "type": "search", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('includes nested dependencies when passed in', async () => { @@ -283,59 +370,65 @@ Array [ }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, -] -`); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "id": "2", - "type": "search", - }, - ], - ], - Array [ Array [ Object { + "attributes": Object {}, "id": "1", + "references": Array [], "type": "index-pattern", }, - ], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "id": "2", + "type": "search", + }, + ], + Object { + "namespace": undefined, + }, + ], + Array [ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('export selected objects throws error when exceeding exportSizeLimit', async () => { diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index e09780574a25..08795e9fc773 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -19,21 +19,24 @@ import Boom from 'boom'; import { createListStream } from '../../../../legacy/utils/streams'; -import { SavedObjectsClientContract } from '../'; +import { SavedObjectsClientContract } from '../types'; import { injectNestedDependencies } from './inject_nested_depdendencies'; import { sortObjects } from './sort_objects'; -interface ObjectToExport { - id: string; - type: string; -} - -interface ExportObjectsOptions { +/** + * Options controlling the export operation. + * @public + */ +export interface SavedObjectsExportOptions { types?: string[]; - objects?: ObjectToExport[]; + objects?: Array<{ + id: string; + type: string; + }>; savedObjectsClient: SavedObjectsClientContract; exportSizeLimit: number; includeReferencesDeep?: boolean; + namespace?: string; } async function fetchObjectsToExport({ @@ -41,17 +44,19 @@ async function fetchObjectsToExport({ types, exportSizeLimit, savedObjectsClient, + namespace, }: { - objects?: ObjectToExport[]; + objects?: SavedObjectsExportOptions['objects']; types?: string[]; exportSizeLimit: number; savedObjectsClient: SavedObjectsClientContract; + namespace?: string; }) { if (objects) { if (objects.length > exportSizeLimit) { throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); } - const bulkGetResult = await savedObjectsClient.bulkGet(objects); + const bulkGetResult = await savedObjectsClient.bulkGet(objects, { namespace }); const erroredObjects = bulkGetResult.saved_objects.filter(obj => !!obj.error); if (erroredObjects.length) { const err = Boom.badRequest(); @@ -67,6 +72,7 @@ async function fetchObjectsToExport({ sortField: '_id', sortOrder: 'asc', perPage: exportSizeLimit, + namespace, }); if (findResponse.total > exportSizeLimit) { throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); @@ -80,17 +86,19 @@ export async function getSortedObjectsForExport({ savedObjectsClient, exportSizeLimit, includeReferencesDeep = false, -}: ExportObjectsOptions) { + namespace, +}: SavedObjectsExportOptions) { const objectsToExport = await fetchObjectsToExport({ types, objects, savedObjectsClient, exportSizeLimit, + namespace, }); const exportedObjects = sortObjects( includeReferencesDeep - ? await injectNestedDependencies(objectsToExport, savedObjectsClient) + ? await injectNestedDependencies(objectsToExport, savedObjectsClient, namespace) : objectsToExport ); diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts index eb52fcbecfda..d994df2af627 100644 --- a/src/core/server/saved_objects/export/index.ts +++ b/src/core/server/saved_objects/export/index.ts @@ -17,4 +17,7 @@ * under the License. */ -export { getSortedObjectsForExport } from './get_sorted_objects_for_export'; +export { + getSortedObjectsForExport, + SavedObjectsExportOptions, +} from './get_sorted_objects_for_export'; diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts index 9a2548952de3..4613553fbd30 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from '../service/saved_objects_client'; +import { SavedObject } from '../types'; import { getObjectReferencesToFetch, injectNestedDependencies, @@ -70,13 +70,13 @@ describe('getObjectReferencesToFetch()', () => { }); const result = getObjectReferencesToFetch(map); expect(result).toMatchInlineSnapshot(` -Array [ - Object { - "id": "1", - "type": "index-pattern", - }, -] -`); + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + ] + `); }); test(`doesn't deal with circular dependencies`, () => { @@ -137,15 +137,15 @@ describe('injectNestedDependencies', () => { ]; const result = await injectNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + ] + `); }); test(`doesn't fetch references that are already fetched`, async () => { @@ -171,27 +171,27 @@ Array [ ]; const result = await injectNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "search", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); }); test('fetches dependencies at least one level deep', async () => { @@ -221,47 +221,50 @@ Array [ }); const result = await injectNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, -] -`); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ Array [ Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, "id": "1", + "references": Array [], "type": "index-pattern", }, - ], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + ] + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('fetches dependencies multiple levels deep', async () => { @@ -336,108 +339,114 @@ Array [ }); const result = await injectNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "5", - "references": Array [ - Object { - "id": "4", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "3", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", - }, - Object { - "attributes": Object {}, - "id": "4", - "references": Array [ - Object { - "id": "2", - "name": "ref_0", - "type": "search", - }, - ], - "type": "visualization", - }, - Object { - "attributes": Object {}, - "id": "3", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "visualization", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, -] -`); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ Array [ Object { + "attributes": Object {}, + "id": "5", + "references": Array [ + Object { + "id": "4", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "3", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", + }, + Object { + "attributes": Object {}, "id": "4", + "references": Array [ + Object { + "id": "2", + "name": "ref_0", + "type": "search", + }, + ], "type": "visualization", }, Object { + "attributes": Object {}, "id": "3", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], "type": "visualization", }, - ], - ], - Array [ - Array [ Object { + "attributes": Object {}, "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], "type": "search", }, Object { + "attributes": Object {}, "id": "1", + "references": Array [], "type": "index-pattern", }, - ], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + ] + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "id": "4", + "type": "visualization", + }, + Object { + "id": "3", + "type": "visualization", + }, + ], + Object { + "namespace": undefined, + }, + ], + Array [ + Array [ + Object { + "id": "2", + "type": "search", + }, + Object { + "id": "1", + "type": "index-pattern", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('throws error when bulkGet returns an error', async () => { @@ -505,52 +514,55 @@ Array [ }); const result = await injectNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "1", - "references": Array [ - Object { - "id": "2", - "name": "ref_0", - "type": "search", - }, - ], - "type": "index-pattern", - }, -] -`); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ Array [ Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, "id": "1", + "references": Array [ + Object { + "id": "2", + "name": "ref_0", + "type": "search", + }, + ], "type": "index-pattern", }, - ], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + ] + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); }); diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.ts index ee9ce781ef9a..279b06f95557 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; -import { SavedObject, SavedObjectsClientContract } from '../service/saved_objects_client'; +import { SavedObject, SavedObjectsClientContract } from '../types'; export function getObjectReferencesToFetch(savedObjectsMap: Map) { const objectsToFetch = new Map(); @@ -34,7 +34,8 @@ export function getObjectReferencesToFetch(savedObjectsMap: Map(); for (const savedObject of savedObjects) { @@ -42,7 +43,7 @@ export async function injectNestedDependencies( } let objectsToFetch = getObjectReferencesToFetch(savedObjectsMap); while (objectsToFetch.length > 0) { - const bulkGetResponse = await savedObjectsClient.bulkGet(objectsToFetch); + const bulkGetResponse = await savedObjectsClient.bulkGet(objectsToFetch, { namespace }); // Check for errors const erroredObjects = bulkGetResponse.saved_objects.filter(obj => !!obj.error); if (erroredObjects.length) { diff --git a/src/core/server/saved_objects/export/sort_objects.ts b/src/core/server/saved_objects/export/sort_objects.ts index 84640db3635e..522146737d9c 100644 --- a/src/core/server/saved_objects/export/sort_objects.ts +++ b/src/core/server/saved_objects/export/sort_objects.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; -import { SavedObject } from '../service/saved_objects_client'; +import { SavedObject } from '../types'; export function sortObjects(savedObjects: SavedObject[]): SavedObject[] { const path = new Set(); diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/collect_saved_objects.ts index 11add36e54fb..65ffd4d9a1d5 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.ts @@ -24,9 +24,9 @@ import { createMapStream, createPromiseFromStreams, } from '../../../../legacy/utils/streams'; -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { createLimitStream } from './create_limit_stream'; -import { ImportError } from './types'; +import { SavedObjectsImportError } from './types'; interface CollectSavedObjectsOptions { readStream: Readable; @@ -41,7 +41,7 @@ export async function collectSavedObjects({ filter, supportedTypes, }: CollectSavedObjectsOptions) { - const errors: ImportError[] = []; + const errors: SavedObjectsImportError[] = []; const collectedObjects: SavedObject[] = await createPromiseFromStreams([ readStream, createLimitStream(objectLimit), diff --git a/src/core/server/saved_objects/import/create_objects_filter.ts b/src/core/server/saved_objects/import/create_objects_filter.ts index aacf8112f255..c48cded00f1e 100644 --- a/src/core/server/saved_objects/import/create_objects_filter.ts +++ b/src/core/server/saved_objects/import/create_objects_filter.ts @@ -17,10 +17,10 @@ * under the License. */ -import { SavedObject } from '../service'; -import { Retry } from './types'; +import { SavedObject } from '../types'; +import { SavedObjectsImportRetry } from './types'; -export function createObjectsFilter(retries: Retry[]) { +export function createObjectsFilter(retries: SavedObjectsImportRetry[]) { const retryKeys = new Set(retries.map(retry => `${retry.type}:${retry.id}`)); return (obj: SavedObject) => { return retryKeys.has(`${obj.type}:${obj.id}`); diff --git a/src/core/server/saved_objects/import/extract_errors.test.ts b/src/core/server/saved_objects/import/extract_errors.test.ts index ad2b1467923a..f97cc661c0bc 100644 --- a/src/core/server/saved_objects/import/extract_errors.test.ts +++ b/src/core/server/saved_objects/import/extract_errors.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { extractErrors } from './extract_errors'; describe('extractErrors()', () => { diff --git a/src/core/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/extract_errors.ts index 6ae9562a1f3a..725e935f6e21 100644 --- a/src/core/server/saved_objects/import/extract_errors.ts +++ b/src/core/server/saved_objects/import/extract_errors.ts @@ -16,15 +16,14 @@ * specific language governing permissions and limitations * under the License. */ - -import { SavedObject } from '../service'; -import { ImportError } from './types'; +import { SavedObject } from '../types'; +import { SavedObjectsImportError } from './types'; export function extractErrors( savedObjectResults: SavedObject[], savedObjectsToImport: SavedObject[] ) { - const errors: ImportError[] = []; + const errors: SavedObjectsImportError[] = []; const originalSavedObjectsMap = new Map(); for (const savedObject of savedObjectsToImport) { originalSavedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject); diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index 80e5cc9a306f..194756462fc7 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -18,7 +18,7 @@ */ import { Readable } from 'stream'; -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { importSavedObjects } from './import_saved_objects'; describe('importSavedObjects()', () => { @@ -86,11 +86,11 @@ describe('importSavedObjects()', () => { supportedTypes: [], }); expect(result).toMatchInlineSnapshot(` -Object { - "success": true, - "successCount": 0, -} -`); + Object { + "success": true, + "successCount": 0, + } + `); }); test('calls bulkCreate without overwrite', async () => { @@ -113,66 +113,151 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "success": true, - "successCount": 4, -} -`); + Object { + "success": true, + "successCount": 4, + } + `); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Index Pattern", - }, - "id": "1", - "migrationVersion": Object {}, - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "title": "My Search", - }, - "id": "2", - "migrationVersion": Object {}, - "references": Array [], - "type": "search", - }, - Object { - "attributes": Object { - "title": "My Visualization", - }, - "id": "3", - "migrationVersion": Object {}, - "references": Array [], - "type": "visualization", - }, - Object { - "attributes": Object { - "title": "My Dashboard", + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "attributes": Object { + "title": "My Index Pattern", + }, + "id": "1", + "migrationVersion": Object {}, + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "title": "My Search", + }, + "id": "2", + "migrationVersion": Object {}, + "references": Array [], + "type": "search", + }, + Object { + "attributes": Object { + "title": "My Visualization", + }, + "id": "3", + "migrationVersion": Object {}, + "references": Array [], + "type": "visualization", + }, + Object { + "attributes": Object { + "title": "My Dashboard", + }, + "id": "4", + "migrationVersion": Object {}, + "references": Array [], + "type": "dashboard", + }, + ], + Object { + "namespace": undefined, + "overwrite": false, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, }, - "id": "4", - "migrationVersion": Object {}, - "references": Array [], - "type": "dashboard", - }, - ], - Object { - "overwrite": false, + ], + } + `); + }); + + test('uses the provided namespace when present', async () => { + const readStream = new Readable({ + objectMode: true, + read() { + savedObjects.forEach(obj => this.push(obj)); + this.push(null); }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + }); + savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] }); + savedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: savedObjects, + }); + const result = await importSavedObjects({ + readStream, + objectLimit: 4, + overwrite: false, + savedObjectsClient, + supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + namespace: 'foo', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "success": true, + "successCount": 4, + } + `); + expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "attributes": Object { + "title": "My Index Pattern", + }, + "id": "1", + "migrationVersion": Object {}, + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "title": "My Search", + }, + "id": "2", + "migrationVersion": Object {}, + "references": Array [], + "type": "search", + }, + Object { + "attributes": Object { + "title": "My Visualization", + }, + "id": "3", + "migrationVersion": Object {}, + "references": Array [], + "type": "visualization", + }, + Object { + "attributes": Object { + "title": "My Dashboard", + }, + "id": "4", + "migrationVersion": Object {}, + "references": Array [], + "type": "dashboard", + }, + ], + Object { + "namespace": "foo", + "overwrite": false, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('calls bulkCreate with overwrite', async () => { @@ -195,66 +280,67 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "success": true, - "successCount": 4, -} -`); + Object { + "success": true, + "successCount": 4, + } + `); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Index Pattern", - }, - "id": "1", - "migrationVersion": Object {}, - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "title": "My Search", - }, - "id": "2", - "migrationVersion": Object {}, - "references": Array [], - "type": "search", - }, - Object { - "attributes": Object { - "title": "My Visualization", - }, - "id": "3", - "migrationVersion": Object {}, - "references": Array [], - "type": "visualization", - }, - Object { - "attributes": Object { - "title": "My Dashboard", + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "attributes": Object { + "title": "My Index Pattern", + }, + "id": "1", + "migrationVersion": Object {}, + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "title": "My Search", + }, + "id": "2", + "migrationVersion": Object {}, + "references": Array [], + "type": "search", + }, + Object { + "attributes": Object { + "title": "My Visualization", + }, + "id": "3", + "migrationVersion": Object {}, + "references": Array [], + "type": "visualization", + }, + Object { + "attributes": Object { + "title": "My Dashboard", + }, + "id": "4", + "migrationVersion": Object {}, + "references": Array [], + "type": "dashboard", + }, + ], + Object { + "namespace": undefined, + "overwrite": true, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, }, - "id": "4", - "migrationVersion": Object {}, - "references": Array [], - "type": "dashboard", - }, - ], - Object { - "overwrite": true, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + ], + } + `); }); test('extracts errors for conflicts', async () => { @@ -284,45 +370,45 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "errors": Array [ - Object { - "error": Object { - "type": "conflict", - }, - "id": "1", - "title": "My Index Pattern", - "type": "index-pattern", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "2", - "title": "My Search", - "type": "search", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "3", - "title": "My Visualization", - "type": "visualization", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "4", - "title": "My Dashboard", - "type": "dashboard", - }, - ], - "success": false, - "successCount": 0, -} -`); + Object { + "errors": Array [ + Object { + "error": Object { + "type": "conflict", + }, + "id": "1", + "title": "My Index Pattern", + "type": "index-pattern", + }, + Object { + "error": Object { + "type": "conflict", + }, + "id": "2", + "title": "My Search", + "type": "search", + }, + Object { + "error": Object { + "type": "conflict", + }, + "id": "3", + "title": "My Visualization", + "type": "visualization", + }, + Object { + "error": Object { + "type": "conflict", + }, + "id": "4", + "title": "My Dashboard", + "type": "dashboard", + }, + ], + "success": false, + "successCount": 0, + } + `); }); test('validates references', async () => { @@ -380,56 +466,59 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "errors": Array [ - Object { - "error": Object { - "blocking": Array [ + Object { + "errors": Array [ Object { - "id": "3", - "type": "visualization", + "error": Object { + "blocking": Array [ + Object { + "id": "3", + "type": "visualization", + }, + ], + "references": Array [ + Object { + "id": "2", + "type": "index-pattern", + }, + ], + "type": "missing_references", + }, + "id": "1", + "title": "My Search", + "type": "search", }, ], - "references": Array [ + "success": false, + "successCount": 0, + } + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "fields": Array [ + "id", + ], + "id": "2", + "type": "index-pattern", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ Object { - "id": "2", - "type": "index-pattern", + "type": "return", + "value": Promise {}, }, ], - "type": "missing_references", - }, - "id": "1", - "title": "My Search", - "type": "search", - }, - ], - "success": false, - "successCount": 0, -} -`); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "fields": Array [ - "id", - ], - "id": "2", - "type": "index-pattern", - }, - ], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + } + `); }); test('validates supported types', async () => { @@ -453,75 +542,76 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "errors": Array [ - Object { - "error": Object { - "type": "unsupported_type", - }, - "id": "1", - "title": "my title", - "type": "wigwags", - }, - ], - "success": false, - "successCount": 4, -} -`); - expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Index Pattern", - }, - "id": "1", - "migrationVersion": Object {}, - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "title": "My Search", - }, - "id": "2", - "migrationVersion": Object {}, - "references": Array [], - "type": "search", - }, - Object { - "attributes": Object { - "title": "My Visualization", + Object { + "errors": Array [ + Object { + "error": Object { + "type": "unsupported_type", + }, + "id": "1", + "title": "my title", + "type": "wigwags", }, - "id": "3", - "migrationVersion": Object {}, - "references": Array [], - "type": "visualization", - }, - Object { - "attributes": Object { - "title": "My Dashboard", + ], + "success": false, + "successCount": 4, + } + `); + expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "attributes": Object { + "title": "My Index Pattern", + }, + "id": "1", + "migrationVersion": Object {}, + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "title": "My Search", + }, + "id": "2", + "migrationVersion": Object {}, + "references": Array [], + "type": "search", + }, + Object { + "attributes": Object { + "title": "My Visualization", + }, + "id": "3", + "migrationVersion": Object {}, + "references": Array [], + "type": "visualization", + }, + Object { + "attributes": Object { + "title": "My Dashboard", + }, + "id": "4", + "migrationVersion": Object {}, + "references": Array [], + "type": "dashboard", + }, + ], + Object { + "namespace": undefined, + "overwrite": false, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, }, - "id": "4", - "migrationVersion": Object {}, - "references": Array [], - "type": "dashboard", - }, - ], - Object { - "overwrite": false, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + ], + } + `); }); }); diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index 10c1350c4c57..ef3b4a214c2c 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -17,26 +17,14 @@ * under the License. */ -import { Readable } from 'stream'; import { collectSavedObjects } from './collect_saved_objects'; import { extractErrors } from './extract_errors'; -import { ImportError } from './types'; +import { + SavedObjectsImportError, + SavedObjectsImportResponse, + SavedObjectsImportOptions, +} from './types'; import { validateReferences } from './validate_references'; -import { SavedObjectsClientContract } from '../'; - -interface ImportSavedObjectsOptions { - readStream: Readable; - objectLimit: number; - overwrite: boolean; - savedObjectsClient: SavedObjectsClientContract; - supportedTypes: string[]; -} - -interface ImportResponse { - success: boolean; - successCount: number; - errors?: ImportError[]; -} export async function importSavedObjects({ readStream, @@ -44,8 +32,9 @@ export async function importSavedObjects({ overwrite, savedObjectsClient, supportedTypes, -}: ImportSavedObjectsOptions): Promise { - let errorAccumulator: ImportError[] = []; + namespace, +}: SavedObjectsImportOptions): Promise { + let errorAccumulator: SavedObjectsImportError[] = []; // Get the objects to import const { @@ -57,7 +46,8 @@ export async function importSavedObjects({ // Validate references const { filteredObjects, errors: validationErrors } = await validateReferences( objectsFromStream, - savedObjectsClient + savedObjectsClient, + namespace ); errorAccumulator = [...errorAccumulator, ...validationErrors]; @@ -73,6 +63,7 @@ export async function importSavedObjects({ // Create objects in bulk const bulkCreateResult = await savedObjectsClient.bulkCreate(filteredObjects, { overwrite, + namespace, }); errorAccumulator = [ ...errorAccumulator, diff --git a/src/core/server/saved_objects/import/index.ts b/src/core/server/saved_objects/import/index.ts index aad06931c330..95fa8aa192f3 100644 --- a/src/core/server/saved_objects/import/index.ts +++ b/src/core/server/saved_objects/import/index.ts @@ -19,3 +19,14 @@ export { importSavedObjects } from './import_saved_objects'; export { resolveImportErrors } from './resolve_import_errors'; +export { + SavedObjectsImportResponse, + SavedObjectsImportError, + SavedObjectsImportOptions, + SavedObjectsImportConflictError, + SavedObjectsImportMissingReferencesError, + SavedObjectsImportUnknownError, + SavedObjectsImportUnsupportedTypeError, + SavedObjectsResolveImportErrorsOptions, + SavedObjectsImportRetry, +} from './types'; diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index d3f36852fd79..9d0e133c5951 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -18,7 +18,7 @@ */ import { Readable } from 'stream'; -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { resolveImportErrors } from './resolve_import_errors'; describe('resolveImportErrors()', () => { @@ -96,11 +96,11 @@ describe('resolveImportErrors()', () => { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "success": true, - "successCount": 0, -} -`); + Object { + "success": true, + "successCount": 0, + } + `); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`); }); @@ -130,36 +130,39 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "success": true, - "successCount": 1, -} -`); + Object { + "success": true, + "successCount": 1, + } + `); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Visualization", + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "attributes": Object { + "title": "My Visualization", + }, + "id": "3", + "migrationVersion": Object {}, + "references": Array [], + "type": "visualization", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, }, - "id": "3", - "migrationVersion": Object {}, - "references": Array [], - "type": "visualization", - }, - ], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + ], + } + `); }); test('works with overwrites', async () => { @@ -188,39 +191,40 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "success": true, - "successCount": 1, -} -`); + Object { + "success": true, + "successCount": 1, + } + `); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Index Pattern", + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "attributes": Object { + "title": "My Index Pattern", + }, + "id": "1", + "migrationVersion": Object {}, + "references": Array [], + "type": "index-pattern", + }, + ], + Object { + "namespace": undefined, + "overwrite": true, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, }, - "id": "1", - "migrationVersion": Object {}, - "references": Array [], - "type": "index-pattern", - }, - ], - Object { - "overwrite": true, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + ], + } + `); }); test('works wtih replaceReferences', async () => { @@ -255,42 +259,45 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "success": true, - "successCount": 1, -} -`); + Object { + "success": true, + "successCount": 1, + } + `); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "attributes": Object { - "title": "My Dashboard", - }, - "id": "4", - "migrationVersion": Object {}, - "references": Array [ + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "attributes": Object { + "title": "My Dashboard", + }, + "id": "4", + "migrationVersion": Object {}, + "references": Array [ + Object { + "id": "13", + "name": "panel_0", + "type": "visualization", + }, + ], + "type": "dashboard", + }, + ], Object { - "id": "13", - "name": "panel_0", - "type": "visualization", + "namespace": undefined, }, ], - "type": "dashboard", - }, - ], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('extracts errors for conflicts', async () => { @@ -324,45 +331,45 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "errors": Array [ - Object { - "error": Object { - "type": "conflict", - }, - "id": "1", - "title": "My Index Pattern", - "type": "index-pattern", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "2", - "title": "My Search", - "type": "search", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "3", - "title": "My Visualization", - "type": "visualization", - }, - Object { - "error": Object { - "type": "conflict", - }, - "id": "4", - "title": "My Dashboard", - "type": "dashboard", - }, - ], - "success": false, - "successCount": 0, -} -`); + Object { + "errors": Array [ + Object { + "error": Object { + "type": "conflict", + }, + "id": "1", + "title": "My Index Pattern", + "type": "index-pattern", + }, + Object { + "error": Object { + "type": "conflict", + }, + "id": "2", + "title": "My Search", + "type": "search", + }, + Object { + "error": Object { + "type": "conflict", + }, + "id": "3", + "title": "My Visualization", + "type": "visualization", + }, + Object { + "error": Object { + "type": "conflict", + }, + "id": "4", + "title": "My Dashboard", + "type": "dashboard", + }, + ], + "success": false, + "successCount": 0, + } + `); }); test('validates references', async () => { @@ -433,56 +440,59 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "errors": Array [ - Object { - "error": Object { - "blocking": Array [ + Object { + "errors": Array [ Object { - "id": "3", - "type": "visualization", + "error": Object { + "blocking": Array [ + Object { + "id": "3", + "type": "visualization", + }, + ], + "references": Array [ + Object { + "id": "2", + "type": "index-pattern", + }, + ], + "type": "missing_references", + }, + "id": "1", + "title": "My Search", + "type": "search", }, ], - "references": Array [ + "success": false, + "successCount": 0, + } + `); + expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + Array [ + Object { + "fields": Array [ + "id", + ], + "id": "2", + "type": "index-pattern", + }, + ], + Object { + "namespace": undefined, + }, + ], + ], + "results": Array [ Object { - "id": "2", - "type": "index-pattern", + "type": "return", + "value": Promise {}, }, ], - "type": "missing_references", - }, - "id": "1", - "title": "My Search", - "type": "search", - }, - ], - "success": false, - "successCount": 0, -} -`); - expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` -[MockFunction] { - "calls": Array [ - Array [ - Array [ - Object { - "fields": Array [ - "id", - ], - "id": "2", - "type": "index-pattern", - }, - ], - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, - ], -} -`); + } + `); }); test('validates object types', async () => { @@ -512,21 +522,67 @@ Object { supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], }); expect(result).toMatchInlineSnapshot(` -Object { - "errors": Array [ - Object { - "error": Object { - "type": "unsupported_type", - }, - "id": "1", - "title": "my title", - "type": "wigwags", - }, - ], - "success": false, - "successCount": 0, -} -`); + Object { + "errors": Array [ + Object { + "error": Object { + "type": "unsupported_type", + }, + "id": "1", + "title": "my title", + "type": "wigwags", + }, + ], + "success": false, + "successCount": 0, + } + `); expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`); }); + + test('uses namespace when provided', async () => { + const readStream = new Readable({ + objectMode: true, + read() { + savedObjects.forEach(obj => this.push(obj)); + this.push(null); + }, + }); + savedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: savedObjects.filter(obj => obj.type === 'index-pattern' && obj.id === '1'), + }); + const result = await resolveImportErrors({ + readStream, + objectLimit: 4, + retries: [ + { + type: 'index-pattern', + id: '1', + overwrite: true, + replaceReferences: [], + }, + ], + savedObjectsClient, + supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'], + namespace: 'foo', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "success": true, + "successCount": 1, + } + `); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + { + attributes: { title: 'My Index Pattern' }, + id: '1', + migrationVersion: {}, + references: [], + type: 'index-pattern', + }, + ], + { namespace: 'foo', overwrite: true } + ); + }); }); diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index 5cd4d2fca740..6f56f283b4ae 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -16,39 +16,27 @@ * specific language governing permissions and limitations * under the License. */ - -import { Readable } from 'stream'; -import { SavedObjectsClientContract } from '../'; import { collectSavedObjects } from './collect_saved_objects'; import { createObjectsFilter } from './create_objects_filter'; import { extractErrors } from './extract_errors'; import { splitOverwrites } from './split_overwrites'; -import { ImportError, Retry } from './types'; +import { + SavedObjectsImportError, + SavedObjectsImportResponse, + SavedObjectsResolveImportErrorsOptions, +} from './types'; import { validateReferences } from './validate_references'; -interface ResolveImportErrorsOptions { - readStream: Readable; - objectLimit: number; - savedObjectsClient: SavedObjectsClientContract; - retries: Retry[]; - supportedTypes: string[]; -} - -interface ImportResponse { - success: boolean; - successCount: number; - errors?: ImportError[]; -} - export async function resolveImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, -}: ResolveImportErrorsOptions): Promise { + namespace, +}: SavedObjectsResolveImportErrorsOptions): Promise { let successCount = 0; - let errorAccumulator: ImportError[] = []; + let errorAccumulator: SavedObjectsImportError[] = []; const filter = createObjectsFilter(retries); // Get the objects to resolve errors @@ -89,7 +77,8 @@ export async function resolveImportErrors({ // Validate references const { filteredObjects, errors: validationErrors } = await validateReferences( objectsToResolve, - savedObjectsClient + savedObjectsClient, + namespace ); errorAccumulator = [...errorAccumulator, ...validationErrors]; @@ -98,6 +87,7 @@ export async function resolveImportErrors({ if (objectsToOverwrite.length) { const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToOverwrite, { overwrite: true, + namespace, }); errorAccumulator = [ ...errorAccumulator, @@ -106,7 +96,9 @@ export async function resolveImportErrors({ successCount += bulkCreateResult.saved_objects.filter(obj => !obj.error).length; } if (objectsToNotOverwrite.length) { - const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToNotOverwrite); + const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToNotOverwrite, { + namespace, + }); errorAccumulator = [ ...errorAccumulator, ...extractErrors(bulkCreateResult.saved_objects, objectsToNotOverwrite), diff --git a/src/core/server/saved_objects/import/split_overwrites.ts b/src/core/server/saved_objects/import/split_overwrites.ts index 5609308f755f..192d43e9edb2 100644 --- a/src/core/server/saved_objects/import/split_overwrites.ts +++ b/src/core/server/saved_objects/import/split_overwrites.ts @@ -17,10 +17,10 @@ * under the License. */ -import { SavedObject } from '../service'; -import { Retry } from './types'; +import { SavedObject } from '../types'; +import { SavedObjectsImportRetry } from './types'; -export function splitOverwrites(savedObjects: SavedObject[], retries: Retry[]) { +export function splitOverwrites(savedObjects: SavedObject[], retries: SavedObjectsImportRetry[]) { const objectsToOverwrite: SavedObject[] = []; const objectsToNotOverwrite: SavedObject[] = []; const overwrites = retries diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index ccefe178f38d..44046378a7b9 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -17,7 +17,14 @@ * under the License. */ -export interface Retry { +import { Readable } from 'stream'; +import { SavedObjectsClientContract } from '../types'; + +/** + * Describes a retry operation for importing a saved object. + * @public + */ +export interface SavedObjectsImportRetry { type: string; id: string; overwrite: boolean; @@ -28,21 +35,37 @@ export interface Retry { }>; } -export interface ConflictError { +/** + * Represents a failure to import due to a conflict. + * @public + */ +export interface SavedObjectsImportConflictError { type: 'conflict'; } -export interface UnsupportedTypeError { +/** + * Represents a failure to import due to having an unsupported saved object type. + * @public + */ +export interface SavedObjectsImportUnsupportedTypeError { type: 'unsupported_type'; } -export interface UnknownError { +/** + * Represents a failure to import due to an unknown reason. + * @public + */ +export interface SavedObjectsImportUnknownError { type: 'unknown'; message: string; statusCode: number; } -export interface MissingReferencesError { +/** + * Represents a failure to import due to missing references. + * @public + */ +export interface SavedObjectsImportMissingReferencesError { type: 'missing_references'; references: Array<{ type: string; @@ -54,9 +77,53 @@ export interface MissingReferencesError { }>; } -export interface ImportError { +/** + * Represents a failure to import. + * @public + */ +export interface SavedObjectsImportError { id: string; type: string; title?: string; - error: ConflictError | UnsupportedTypeError | MissingReferencesError | UnknownError; + error: + | SavedObjectsImportConflictError + | SavedObjectsImportUnsupportedTypeError + | SavedObjectsImportMissingReferencesError + | SavedObjectsImportUnknownError; +} + +/** + * The response describing the result of an import. + * @public + */ +export interface SavedObjectsImportResponse { + success: boolean; + successCount: number; + errors?: SavedObjectsImportError[]; +} + +/** + * Options to control the import operation. + * @public + */ +export interface SavedObjectsImportOptions { + readStream: Readable; + objectLimit: number; + overwrite: boolean; + savedObjectsClient: SavedObjectsClientContract; + supportedTypes: string[]; + namespace?: string; +} + +/** + * Options to control the "resolve import" operation. + * @public + */ +export interface SavedObjectsResolveImportErrorsOptions { + readStream: Readable; + objectLimit: number; + savedObjectsClient: SavedObjectsClientContract; + retries: SavedObjectsImportRetry[]; + supportedTypes: string[]; + namespace?: string; } diff --git a/src/core/server/saved_objects/import/validate_references.test.ts b/src/core/server/saved_objects/import/validate_references.test.ts index e69ca99fb10f..1a558b3d82b3 100644 --- a/src/core/server/saved_objects/import/validate_references.test.ts +++ b/src/core/server/saved_objects/import/validate_references.test.ts @@ -107,6 +107,9 @@ describe('getNonExistingReferenceAsKeys()', () => { "type": "index-pattern", }, ], + Object { + "namespace": undefined, + }, ], ], "results": Array [ @@ -206,6 +209,9 @@ describe('getNonExistingReferenceAsKeys()', () => { "type": "search", }, ], + Object { + "namespace": undefined, + }, ], ], "results": Array [ @@ -434,6 +440,9 @@ Object { "type": "search", }, ], + Object { + "namespace": undefined, + }, ], ], "results": Array [ diff --git a/src/core/server/saved_objects/import/validate_references.ts b/src/core/server/saved_objects/import/validate_references.ts index 2e3c1ef5293b..4d9ee59f9df1 100644 --- a/src/core/server/saved_objects/import/validate_references.ts +++ b/src/core/server/saved_objects/import/validate_references.ts @@ -18,8 +18,8 @@ */ import Boom from 'boom'; -import { SavedObject, SavedObjectsClientContract } from '../'; -import { ImportError } from './types'; +import { SavedObject, SavedObjectsClientContract } from '../types'; +import { SavedObjectsImportError } from './types'; const REF_TYPES_TO_VLIDATE = ['index-pattern', 'search']; @@ -29,7 +29,8 @@ function filterReferencesToValidate({ type }: { type: string }) { export async function getNonExistingReferenceAsKeys( savedObjects: SavedObject[], - savedObjectsClient: SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract, + namespace?: string ) { const collector = new Map(); // Collect all references within objects @@ -50,7 +51,7 @@ export async function getNonExistingReferenceAsKeys( // Fetch references to see if they exist const bulkGetOpts = Array.from(collector.values()).map(obj => ({ ...obj, fields: ['id'] })); - const bulkGetResponse = await savedObjectsClient.bulkGet(bulkGetOpts); + const bulkGetResponse = await savedObjectsClient.bulkGet(bulkGetOpts, { namespace }); // Error handling const erroredObjects = bulkGetResponse.saved_objects.filter( @@ -77,12 +78,14 @@ export async function getNonExistingReferenceAsKeys( export async function validateReferences( savedObjects: SavedObject[], - savedObjectsClient: SavedObjectsClientContract + savedObjectsClient: SavedObjectsClientContract, + namespace?: string ) { - const errorMap: { [key: string]: ImportError } = {}; + const errorMap: { [key: string]: SavedObjectsImportError } = {}; const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys( savedObjects, - savedObjectsClient + savedObjectsClient, + namespace ); // Filter out objects with missing references, add to error object diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index e6e9e2d26600..1a667d6978f1 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -22,3 +22,11 @@ export * from './service'; export { SavedObjectsSchema } from './schema'; export { SavedObjectsManagement } from './management'; + +export * from './import'; + +export { getSortedObjectsForExport, SavedObjectsExportOptions } from './export'; + +export { SavedObjectsSerializer, RawDoc as SavedObjectsRawDoc } from './serialization'; + +export { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; diff --git a/src/core/server/saved_objects/management/management.ts b/src/core/server/saved_objects/management/management.ts index c2c6789615b7..7b5274da91fc 100644 --- a/src/core/server/saved_objects/management/management.ts +++ b/src/core/server/saved_objects/management/management.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; interface SavedObjectsManagementTypeDefinition { isImportableAndExportable?: boolean; diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts new file mode 100644 index 000000000000..d596ade8f476 --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts @@ -0,0 +1,168 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { createIndexMap } from './build_index_map'; +import { ObjectToConfigAdapter } from '../../../config'; +import { SavedObjectsSchema } from '../../schema'; + +test('mappings without index pattern goes to default index', () => { + const result = createIndexMap({ + config: new ObjectToConfigAdapter({}), + kibanaIndexName: '.kibana', + schema: new SavedObjectsSchema({ + type1: { + isNamespaceAgnostic: false, + }, + }), + indexMap: { + type1: { + properties: { + field1: { + type: 'string', + }, + }, + }, + }, + }); + expect(result).toEqual({ + '.kibana': { + typeMappings: { + type1: { + properties: { + field1: { + type: 'string', + }, + }, + }, + }, + }, + }); +}); + +test(`mappings with custom index pattern doesn't go to default index`, () => { + const result = createIndexMap({ + config: new ObjectToConfigAdapter({}), + kibanaIndexName: '.kibana', + schema: new SavedObjectsSchema({ + type1: { + isNamespaceAgnostic: false, + indexPattern: '.other_kibana', + }, + }), + indexMap: { + type1: { + properties: { + field1: { + type: 'string', + }, + }, + }, + }, + }); + expect(result).toEqual({ + '.other_kibana': { + typeMappings: { + type1: { + properties: { + field1: { + type: 'string', + }, + }, + }, + }, + }, + }); +}); + +test('creating a script gets added to the index pattern', () => { + const result = createIndexMap({ + config: new ObjectToConfigAdapter({}), + kibanaIndexName: '.kibana', + schema: new SavedObjectsSchema({ + type1: { + isNamespaceAgnostic: false, + indexPattern: '.other_kibana', + convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, + }, + }), + indexMap: { + type1: { + properties: { + field1: { + type: 'string', + }, + }, + }, + }, + }); + expect(result).toEqual({ + '.other_kibana': { + script: `ctx._id = ctx._source.type + ':' + ctx._id`, + typeMappings: { + type1: { + properties: { + field1: { + type: 'string', + }, + }, + }, + }, + }, + }); +}); + +test('throws when two scripts are defined for an index pattern', () => { + const defaultIndex = '.kibana'; + const schema = new SavedObjectsSchema({ + type1: { + isNamespaceAgnostic: false, + convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, + }, + type2: { + isNamespaceAgnostic: false, + convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, + }, + }); + const indexMap = { + type1: { + properties: { + field1: { + type: 'string', + }, + }, + }, + type2: { + properties: { + field1: { + type: 'string', + }, + }, + }, + }; + expect(() => + createIndexMap({ + config: new ObjectToConfigAdapter({}), + kibanaIndexName: defaultIndex, + schema, + indexMap, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"convertToAliasScript has been defined more than once for index pattern \\".kibana\\""` + ); +}); diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.ts b/src/core/server/saved_objects/migrations/core/build_index_map.ts index 365c79692ba0..d7a26b7728f4 100644 --- a/src/core/server/saved_objects/migrations/core/build_index_map.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.ts @@ -17,24 +17,49 @@ * under the License. */ +import { Config } from '../../../config'; import { MappingProperties } from '../../mappings'; -import { SavedObjectsSchemaDefinition } from '../../schema'; +import { SavedObjectsSchema } from '../../schema'; + +export interface CreateIndexMapOptions { + config: Config; + kibanaIndexName: string; + schema: SavedObjectsSchema; + indexMap: MappingProperties; +} + +export interface IndexMap { + [index: string]: { + typeMappings: MappingProperties; + script?: string; + }; +} /* * This file contains logic to convert savedObjectSchemas into a dictonary of indexes and documents */ -export function createIndexMap( - defaultIndex: string, - savedObjectSchemas: SavedObjectsSchemaDefinition, - indexMap: MappingProperties -) { - const map: { [index: string]: MappingProperties } = {}; +export function createIndexMap({ + config, + kibanaIndexName, + schema, + indexMap, +}: CreateIndexMapOptions) { + const map: IndexMap = {}; Object.keys(indexMap).forEach(type => { - const indexPattern = (savedObjectSchemas[type] || {}).indexPattern || defaultIndex; + const script = schema.getConvertToAliasScript(type); + // Defaults to kibanaIndexName if indexPattern isn't defined + const indexPattern = schema.getIndexForType(config, type) || kibanaIndexName; if (!map.hasOwnProperty(indexPattern as string)) { - map[indexPattern] = {}; + map[indexPattern] = { typeMappings: {} }; + } + map[indexPattern].typeMappings[type] = indexMap[type]; + if (script && map[indexPattern].script) { + throw Error( + `convertToAliasScript has been defined more than once for index pattern "${indexPattern}"` + ); + } else if (script) { + map[indexPattern].script = script; } - map[indexPattern][type] = indexMap[type]; }); return map; } diff --git a/src/core/server/saved_objects/migrations/core/call_cluster.ts b/src/core/server/saved_objects/migrations/core/call_cluster.ts index f5b4f787a61d..628f2785e6c6 100644 --- a/src/core/server/saved_objects/migrations/core/call_cluster.ts +++ b/src/core/server/saved_objects/migrations/core/call_cluster.ts @@ -90,6 +90,10 @@ export interface ReindexOpts { body: { dest: IndexOpts; source: IndexOpts & { size: number }; + script?: { + source: string; + lang: 'painless'; + }; }; refresh: boolean; waitForCompletion: boolean; diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 578fe49b2d3c..0576f1d22199 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -65,10 +65,13 @@ import _ from 'lodash'; import cloneDeep from 'lodash.clonedeep'; import Semver from 'semver'; import { RawSavedObjectDoc } from '../../serialization'; -import { SavedObjectsMigrationVersion } from '../../'; -import { LogFn, Logger, MigrationLogger } from './migration_logger'; +import { SavedObjectsMigrationVersion } from '../../types'; +import { LogFn, SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; -export type TransformFn = (doc: RawSavedObjectDoc, log?: Logger) => RawSavedObjectDoc; +export type TransformFn = ( + doc: RawSavedObjectDoc, + log?: SavedObjectsMigrationLogger +) => RawSavedObjectDoc; type ValidateDoc = (doc: RawSavedObjectDoc) => void; @@ -204,7 +207,10 @@ function validateMigrationDefinition(migrations: MigrationDefinition) { * From: { type: { version: fn } } * To: { type: { latestVersion: string, transforms: [{ version: string, transform: fn }] } } */ -function buildActiveMigrations(migrations: MigrationDefinition, log: Logger): ActiveMigrations { +function buildActiveMigrations( + migrations: MigrationDefinition, + log: SavedObjectsMigrationLogger +): ActiveMigrations { return _.mapValues(migrations, (versions, prop) => { const transforms = Object.entries(versions) .map(([version, transform]) => ({ @@ -293,7 +299,12 @@ function markAsUpToDate(doc: RawSavedObjectDoc, migrations: ActiveMigrations) { * If a specific transform function fails, this tacks on a bit of information * about the document and transform that caused the failure. */ -function wrapWithTry(version: string, prop: string, transform: TransformFn, log: Logger) { +function wrapWithTry( + version: string, + prop: string, + transform: TransformFn, + log: SavedObjectsMigrationLogger +) { return function tryTransformDoc(doc: RawSavedObjectDoc) { try { const result = transform(doc, log); diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts index 65df2fd580d8..393cbb7fbb2a 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts @@ -231,6 +231,10 @@ describe('ElasticIndex', () => { body: { dest: { index: '.ze-index' }, source: { index: '.muchacha' }, + script: { + source: `ctx._id = ctx._source.type + ':' + ctx._id`, + lang: 'painless', + }, }, refresh: true, waitForCompletion: false, @@ -267,7 +271,13 @@ describe('ElasticIndex', () => { properties: { foo: { type: 'keyword' } }, }, }; - await Index.convertToAlias(callCluster as any, info, '.muchacha', 10); + await Index.convertToAlias( + callCluster as any, + info, + '.muchacha', + 10, + `ctx._id = ctx._source.type + ':' + ctx._id` + ); expect(callCluster.mock.calls.map(([path]) => path)).toEqual([ 'indices.create', diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index 9606a46edef9..e7621d88f78e 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -24,7 +24,7 @@ import _ from 'lodash'; import { IndexMapping } from '../../mappings'; -import { SavedObjectsMigrationVersion } from '../../'; +import { SavedObjectsMigrationVersion } from '../../types'; import { AliasAction, CallCluster, NotFound, RawDoc, ShardsInfo } from './call_cluster'; const settings = { number_of_shards: 1, auto_expand_replicas: '0-1' }; @@ -228,14 +228,15 @@ export async function convertToAlias( callCluster: CallCluster, info: FullIndexInfo, alias: string, - batchSize: number + batchSize: number, + script?: string ) { await callCluster('indices.create', { body: { mappings: info.mappings, settings }, index: info.indexName, }); - await reindex(callCluster, alias, info.indexName, batchSize); + await reindex(callCluster, alias, info.indexName, batchSize, script); await claimAlias(callCluster, info.indexName, alias, [{ remove_index: { index: alias } }]); } @@ -316,7 +317,13 @@ function assertResponseIncludeAllShards({ _shards }: { _shards: ShardsInfo }) { /** * Reindexes from source to dest, polling for the reindex completion. */ -async function reindex(callCluster: CallCluster, source: string, dest: string, batchSize: number) { +async function reindex( + callCluster: CallCluster, + source: string, + dest: string, + batchSize: number, + script?: string +) { // We poll instead of having the request wait for completion, as for large indices, // the request times out on the Elasticsearch side of things. We have a relatively tight // polling interval, as the request is fairly efficent, and we don't @@ -326,6 +333,12 @@ async function reindex(callCluster: CallCluster, source: string, dest: string, b body: { dest: { index: dest }, source: { index: source, size: batchSize }, + script: script + ? { + source: script, + lang: 'painless', + } + : undefined, }, refresh: true, waitForCompletion: false, diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index 7fc2bcfb7260..c75fa68572c7 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -176,7 +176,7 @@ async function migrateSourceToDest(context: Context) { if (!source.aliases[alias]) { log.info(`Reindexing ${alias} to ${source.indexName}`); - await Index.convertToAlias(callCluster, source, alias, batchSize); + await Index.convertToAlias(callCluster, source, alias, batchSize, context.convertToAliasScript); } const read = Index.reader(callCluster, source.indexName, { batchSize, scrollDuration }); diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index f3c4b271c3a7..633bccf8acee 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -30,7 +30,7 @@ import { buildActiveMappings } from './build_active_mappings'; import { CallCluster } from './call_cluster'; import { VersionedTransformer } from './document_migrator'; import { fetchInfo, FullIndexInfo } from './elastic_index'; -import { LogFn, Logger, MigrationLogger } from './migration_logger'; +import { LogFn, SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; export interface MigrationOpts { batchSize: number; @@ -42,6 +42,7 @@ export interface MigrationOpts { mappingProperties: MappingProperties; documentMigrator: VersionedTransformer; serializer: SavedObjectsSerializer; + convertToAliasScript?: string; /** * If specified, templates matching the specified pattern will be removed @@ -56,12 +57,13 @@ export interface Context { source: FullIndexInfo; dest: FullIndexInfo; documentMigrator: VersionedTransformer; - log: Logger; + log: SavedObjectsMigrationLogger; batchSize: number; pollInterval: number; scrollDuration: string; serializer: SavedObjectsSerializer; obsoleteIndexTemplatePattern?: string; + convertToAliasScript?: string; } /** @@ -87,6 +89,7 @@ export async function migrationContext(opts: MigrationOpts): Promise { scrollDuration: opts.scrollDuration, serializer: opts.serializer, obsoleteIndexTemplatePattern: opts.obsoleteIndexTemplatePattern, + convertToAliasScript: opts.convertToAliasScript, }; } diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts index c424b88335a9..ddd82edd9344 100644 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts +++ b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts @@ -35,7 +35,7 @@ */ import _ from 'lodash'; -import { Logger } from './migration_logger'; +import { SavedObjectsMigrationLogger } from './migration_logger'; const DEFAULT_POLL_INTERVAL = 15000; @@ -52,7 +52,7 @@ export type MigrationResult = interface Opts { runMigration: () => Promise; isMigrated: () => Promise; - log: Logger; + log: SavedObjectsMigrationLogger; pollInterval?: number; } @@ -86,7 +86,7 @@ export async function coordinateMigration(opts: Opts): Promise * and is the cue for us to fall into a polling loop, waiting for some * other Kibana instance to complete the migration. */ -function handleIndexExists(error: any, log: Logger) { +function handleIndexExists(error: any, log: SavedObjectsMigrationLogger) { const isIndexExistsError = _.get(error, 'body.error.type') === 'resource_already_exists_exception'; if (!isIndexExistsError) { diff --git a/src/core/server/saved_objects/migrations/core/migration_logger.ts b/src/core/server/saved_objects/migrations/core/migration_logger.ts index 8b9e3f0a21ac..9c98b7d85a8d 100644 --- a/src/core/server/saved_objects/migrations/core/migration_logger.ts +++ b/src/core/server/saved_objects/migrations/core/migration_logger.ts @@ -24,13 +24,14 @@ export type LogFn = (path: string[], message: string) => void; -export interface Logger { +/** @public */ +export interface SavedObjectsMigrationLogger { debug: (msg: string) => void; info: (msg: string) => void; warning: (msg: string) => void; } -export class MigrationLogger implements Logger { +export class MigrationLogger implements SavedObjectsMigrationLogger { private log: LogFn; constructor(log: LogFn) { diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 7237d62dca6e..9fc8afd35604 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -84,6 +84,30 @@ describe('KibanaMigrator', () => { const migrationResults = await new KibanaMigrator({ kbnServer }).awaitMigration(); expect(migrationResults.length).toEqual(2); }); + + it('only handles and deletes index templates once', async () => { + const { kbnServer } = mockKbnServer(); + const clusterStub = jest.fn(() => ({ status: 404 })); + const waitUntilReady = jest.fn(async () => undefined); + + kbnServer.server.plugins.elasticsearch = { + waitUntilReady, + getCluster() { + return { + callWithInternalUser: clusterStub, + }; + }, + }; + + await new KibanaMigrator({ kbnServer }).awaitMigration(); + + // callCluster with "cat.templates" is called by "deleteIndexTemplates" function + // and should only be done once + const callClusterCommands = clusterStub.mock.calls + .map(([callClusterPath]) => callClusterPath) + .filter(callClusterPath => callClusterPath === 'cat.templates'); + expect(callClusterCommands.length).toBe(1); + }); }); }); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index b2a03a7623bf..78a8507e0c41 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -31,6 +31,7 @@ import { docValidator } from '../../validation'; import { buildActiveMappings, CallCluster, IndexMigrator, LogFn } from '../core'; import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; import { createIndexMap } from '../core/build_index_map'; +import { Config } from '../../../config'; export interface KbnServer { server: Server; version: string; @@ -92,12 +93,14 @@ export class KibanaMigrator { // Wait until elasticsearch is green... await server.plugins.elasticsearch.waitUntilReady(); - const config = server.config(); - const indexMap = createIndexMap( - config.get('kibana.index'), - this.kbnServer.uiExports.savedObjectSchemas, - this.mappingProperties - ); + const config = server.config() as Config; + const kibanaIndexName = config.get('kibana.index'); + const indexMap = createIndexMap({ + config, + kibanaIndexName, + indexMap: this.mappingProperties, + schema: this.schema, + }); const migrators = Object.keys(indexMap).map(index => { return new IndexMigrator({ @@ -106,11 +109,14 @@ export class KibanaMigrator { documentMigrator: this.documentMigrator, index, log: this.log, - mappingProperties: indexMap[index], + mappingProperties: indexMap[index].typeMappings, pollInterval: config.get('migrations.pollInterval'), scrollDuration: config.get('migrations.scrollDuration'), serializer: this.serializer, - obsoleteIndexTemplatePattern: 'kibana_index_template*', + // Only necessary for the migrator of the kibana index. + obsoleteIndexTemplatePattern: + index === kibanaIndexName ? 'kibana_index_template*' : undefined, + convertToAliasScript: indexMap[index].script, }); }); @@ -126,6 +132,7 @@ export class KibanaMigrator { private mappingProperties: MappingProperties; private log: LogFn; private serializer: SavedObjectsSerializer; + private readonly schema: SavedObjectsSchema; /** * Creates an instance of KibanaMigrator. @@ -137,9 +144,8 @@ export class KibanaMigrator { constructor({ kbnServer }: { kbnServer: KbnServer }) { this.kbnServer = kbnServer; - this.serializer = new SavedObjectsSerializer( - new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas) - ); + this.schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas); + this.serializer = new SavedObjectsSerializer(this.schema); this.mappingProperties = mergeProperties(kbnServer.uiExports.savedObjectMappings || []); diff --git a/src/core/server/saved_objects/schema/schema.mock.ts b/src/core/server/saved_objects/schema/schema.mock.ts index 7fec7d54294d..89c318f087ab 100644 --- a/src/core/server/saved_objects/schema/schema.mock.ts +++ b/src/core/server/saved_objects/schema/schema.mock.ts @@ -25,6 +25,7 @@ const createSchemaMock = () => { getIndexForType: jest.fn().mockReturnValue('.kibana-test'), isHiddenType: jest.fn().mockReturnValue(false), isNamespaceAgnostic: jest.fn((type: string) => type === 'global'), + getConvertToAliasScript: jest.fn().mockReturnValue(undefined), }; return mocked; }; diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts index 6756feeb15a0..09676fb50401 100644 --- a/src/core/server/saved_objects/schema/schema.ts +++ b/src/core/server/saved_objects/schema/schema.ts @@ -16,10 +16,14 @@ * specific language governing permissions and limitations * under the License. */ + +import { Config } from '../../config'; + interface SavedObjectsSchemaTypeDefinition { isNamespaceAgnostic: boolean; hidden?: boolean; - indexPattern?: string; + indexPattern?: ((config: Config) => string) | string; + convertToAliasScript?: string; } export interface SavedObjectsSchemaDefinition { @@ -40,14 +44,21 @@ export class SavedObjectsSchema { return false; } - public getIndexForType(type: string): string | undefined { + public getIndexForType(config: Config, type: string): string | undefined { if (this.definition != null && this.definition.hasOwnProperty(type)) { - return this.definition[type].indexPattern; + const { indexPattern } = this.definition[type]; + return typeof indexPattern === 'function' ? indexPattern(config) : indexPattern; } else { return undefined; } } + public getConvertToAliasScript(type: string): string | undefined { + if (this.definition != null && this.definition.hasOwnProperty(type)) { + return this.definition[type].convertToAliasScript; + } + } + public isNamespaceAgnostic(type: string) { // if no plugins have registered a uiExports.savedObjectSchemas, // this.schema will be undefined, and no types are namespace agnostic diff --git a/src/core/server/saved_objects/serialization/index.ts b/src/core/server/saved_objects/serialization/index.ts index 86a448ba8a5b..bd875db2001f 100644 --- a/src/core/server/saved_objects/serialization/index.ts +++ b/src/core/server/saved_objects/serialization/index.ts @@ -27,10 +27,7 @@ import uuid from 'uuid'; import { SavedObjectsSchema } from '../schema'; import { decodeVersion, encodeVersion } from '../version'; -import { - SavedObjectsMigrationVersion, - SavedObjectReference, -} from '../service/saved_objects_client'; +import { SavedObjectsMigrationVersion, SavedObjectReference } from '../types'; /** * A raw document as represented directly in the saved object index. diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 697e1d2d4147..386539e755d9 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -17,8 +17,13 @@ * under the License. */ +import { Readable } from 'stream'; import { ScopedSavedObjectsClientProvider } from './lib'; import { SavedObjectsClient } from './saved_objects_client'; +import { SavedObjectsExportOptions } from '../export'; +import { SavedObjectsImportOptions, SavedObjectsImportResponse } from '../import'; +import { SavedObjectsSchema } from '../schema'; +import { SavedObjectsResolveImportErrorsOptions } from '../import/types'; /** * @public @@ -31,12 +36,22 @@ export interface SavedObjectsService { getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; SavedObjectsClient: typeof SavedObjectsClient; types: string[]; + schema: SavedObjectsSchema; getSavedObjectsRepository(...rest: any[]): any; + importExport: { + objectLimit: number; + importSavedObjects(options: SavedObjectsImportOptions): Promise; + resolveImportErrors( + options: SavedObjectsResolveImportErrorsOptions + ): Promise; + getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; + }; } export { SavedObjectsRepository, ScopedSavedObjectsClientProvider, + SavedObjectsClientProviderOptions, SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, SavedObjectsErrorHelpers, diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index 19fdc3d75f60..d987737c2ffa 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -22,6 +22,7 @@ export { SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, ScopedSavedObjectsClientProvider, + SavedObjectsClientProviderOptions, } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index eb41df3a19d2..e93d9e404750 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -27,21 +27,24 @@ import { SavedObjectsErrorHelpers } from './errors'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; import { SavedObjectsSchema } from '../../schema'; import { KibanaMigrator } from '../../migrations'; +import { Config } from '../../../config'; import { SavedObjectsSerializer, SanitizedSavedObjectDoc, RawDoc } from '../../serialization'; import { - SavedObject, - SavedObjectAttributes, - SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkResponse, SavedObjectsCreateOptions, - SavedObjectsFindOptions, SavedObjectsFindResponse, - SavedObjectsMigrationVersion, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from '../saved_objects_client'; +import { + SavedObject, + SavedObjectAttributes, + SavedObjectsBaseOptions, + SavedObjectsFindOptions, + SavedObjectsMigrationVersion, +} from '../../types'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -64,6 +67,7 @@ const isLeft = (either: Either): either is Left => { export interface SavedObjectsRepositoryOptions { index: string; + config: Config; mappings: IndexMapping; callCluster: CallCluster; schema: SavedObjectsSchema; @@ -80,6 +84,7 @@ export interface IncrementCounterOptions extends SavedObjectsBaseOptions { export class SavedObjectsRepository { private _migrator: KibanaMigrator; private _index: string; + private _config: Config; private _mappings: IndexMapping; private _schema: SavedObjectsSchema; private _allowedTypes: string[]; @@ -90,6 +95,7 @@ export class SavedObjectsRepository { constructor(options: SavedObjectsRepositoryOptions) { const { index, + config, mappings, callCluster, schema, @@ -108,6 +114,7 @@ export class SavedObjectsRepository { // to returning them. this._migrator = migrator; this._index = index; + this._config = config; this._mappings = mappings; this._schema = schema; if (allowedTypes.length === 0) { @@ -741,7 +748,7 @@ export class SavedObjectsRepository { * @param type - the type */ private getIndexForType(type: string) { - return this._schema.getIndexForType(type) || this._index; + return this._schema.getIndexForType(this._config, type) || this._index; } /** @@ -753,7 +760,7 @@ export class SavedObjectsRepository { */ private getIndicesForTypes(types: string[]) { const unique = (array: string[]) => [...new Set(array)]; - return unique(types.map(t => this._schema.getIndexForType(t) || this._index)); + return unique(types.map(t => this._schema.getIndexForType(this._config, t) || this._index)); } private _getCurrentTime() { diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts index fc0a3ea64c7a..0e93f9c443f1 100644 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts @@ -17,23 +17,39 @@ * under the License. */ import { PriorityCollection } from './priority_collection'; -import { SavedObjectsClientContract } from '..'; +import { SavedObjectsClientContract } from '../../types'; +/** + * Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. + * @public + */ export interface SavedObjectsClientWrapperOptions { client: SavedObjectsClientContract; request: Request; } +/** + * Describes the factory used to create instances of Saved Objects Client Wrappers. + * @public + */ export type SavedObjectsClientWrapperFactory = ( options: SavedObjectsClientWrapperOptions ) => SavedObjectsClientContract; +/** + * Describes the factory used to create instances of the Saved Objects Client. + * @public + */ export type SavedObjectsClientFactory = ({ request, }: { request: Request; }) => SavedObjectsClientContract; +/** + * Options to control the creation of the Saved Objects Client. + * @public + */ export interface SavedObjectsClientProviderOptions { excludedWrappers?: string[]; } diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index 4d1ceeaf552b..60bd5f96ff94 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObjectsClientContract } from './saved_objects_client'; +import { SavedObjectsClientContract } from '../types'; import { SavedObjectsErrorHelpers } from './lib/errors'; const create = (): jest.Mocked => ({ diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index adc25a6d045e..039579c5a2d1 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -18,20 +18,18 @@ */ import { SavedObjectsRepository } from './lib'; - +import { + SavedObject, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsMigrationVersion, + SavedObjectsBaseOptions, + SavedObjectsFindOptions, +} from '../types'; import { SavedObjectsErrorHelpers } from './lib/errors'; type Omit = Pick>; -/** - * - * @public - */ -export interface SavedObjectsBaseOptions { - /** Specify the namespace for this operation */ - namespace?: string; -} - /** * * @public @@ -41,6 +39,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { id?: string; /** Overwrite existing documents (defaults to false) */ overwrite?: boolean; + /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; references?: SavedObjectReference[]; } @@ -54,6 +53,7 @@ export interface SavedObjectsBulkCreateObject } /** + * Return type of the Saved Objects `find()` method. * - * @public - */ -export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { - type?: string | string[]; - page?: number; - perPage?: number; - sortField?: string; - sortOrder?: string; - fields?: string[]; - search?: string; - /** see Elasticsearch Simple Query String Query field argument for more information */ - searchFields?: string[]; - hasReference?: { type: string; id: string }; - defaultSearchOperator?: 'AND' | 'OR'; -} - -/** + * *Note*: this type is different between the Public and Server Saved Objects + * clients. * * @public */ @@ -132,135 +118,6 @@ export interface SavedObjectsUpdateResponse; } -/** - * A dictionary of saved object type -> version used to determine - * what migrations need to be applied to a saved object. - * - * @public - */ -export interface SavedObjectsMigrationVersion { - [pluginName: string]: string; -} - -/** - * - * @public - */ -export type SavedObjectAttribute = - | string - | number - | boolean - | null - | undefined - | SavedObjectAttributes - | SavedObjectAttributes[]; - -/** - * - * @public - */ -export interface SavedObjectAttributes { - [key: string]: SavedObjectAttribute | SavedObjectAttribute[]; -} - -/** - * - * @public - */ -export interface SavedObject { - id: string; - type: string; - version?: string; - updated_at?: string; - error?: { - message: string; - statusCode: number; - }; - attributes: T; - references: SavedObjectReference[]; - migrationVersion?: SavedObjectsMigrationVersion; -} - -/** - * A reference to another saved object. - * - * @public - */ -export interface SavedObjectReference { - name: string; - type: string; - id: string; -} - -/** - * ## SavedObjectsClient errors - * - * Since the SavedObjectsClient has its hands in everything we - * are a little paranoid about the way we present errors back to - * to application code. Ideally, all errors will be either: - * - * 1. Caused by bad implementation (ie. undefined is not a function) and - * as such unpredictable - * 2. An error that has been classified and decorated appropriately - * by the decorators in {@link SavedObjectsErrorHelpers} - * - * Type 1 errors are inevitable, but since all expected/handle-able errors - * should be Type 2 the `isXYZError()` helpers exposed at - * `SavedObjectsErrorHelpers` should be used to understand and manage error - * responses from the `SavedObjectsClient`. - * - * Type 2 errors are decorated versions of the source error, so if - * the elasticsearch client threw an error it will be decorated based - * on its type. That means that rather than looking for `error.body.error.type` or - * doing substring checks on `error.body.error.reason`, just use the helpers to - * understand the meaning of the error: - * - * ```js - * if (SavedObjectsErrorHelpers.isNotFoundError(error)) { - * // handle 404 - * } - * - * if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { - * // 401 handling should be automatic, but in case you wanted to know - * } - * - * // always rethrow the error unless you handle it - * throw error; - * ``` - * - * ### 404s from missing index - * - * From the perspective of application code and APIs the SavedObjectsClient is - * a black box that persists objects. One of the internal details that users have - * no control over is that we use an elasticsearch index for persistance and that - * index might be missing. - * - * At the time of writing we are in the process of transitioning away from the - * operating assumption that the SavedObjects index is always available. Part of - * this transition is handling errors resulting from an index missing. These used - * to trigger a 500 error in most cases, and in others cause 404s with different - * error messages. - * - * From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The - * object the request/call was targeting could not be found. This is why #14141 - * takes special care to ensure that 404 errors are generic and don't distinguish - * between index missing or document missing. - * - * ### 503s from missing index - * - * Unlike all other methods, create requests are supposed to succeed even when - * the Kibana index does not exist because it will be automatically created by - * elasticsearch. When that is not the case it is because Elasticsearch's - * `action.auto_create_index` setting prevents it from being created automatically - * so we throw a special 503 with the intention of informing the user that their - * Elasticsearch settings need to be updated. - * - * See {@link SavedObjectsErrorHelpers} - * - * @public - */ -export type SavedObjectsClientContract = Pick; - /** * * @internal diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts new file mode 100644 index 000000000000..0966874aa435 --- /dev/null +++ b/src/core/server/saved_objects/types.ts @@ -0,0 +1,203 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { SavedObjectsClient } from './service/saved_objects_client'; + +/** + * Information about the migrations that have been applied to this SavedObject. + * When Kibana starts up, KibanaMigrator detects outdated documents and + * migrates them based on this value. For each migration that has been applied, + * the plugin's name is used as a key and the latest migration version as the + * value. + * + * @example + * migrationVersion: { + * dashboard: '7.1.1', + * space: '6.6.6', + * } + * + * @public + */ +export interface SavedObjectsMigrationVersion { + [pluginName: string]: string; +} + +/** + * + * @public + */ +export type SavedObjectAttribute = + | string + | number + | boolean + | null + | undefined + | SavedObjectAttributes + | SavedObjectAttributes[]; + +/** + * The data for a Saved Object is stored in the `attributes` key as either an + * object or an array of objects. + * + * @public + */ +export interface SavedObjectAttributes { + [key: string]: SavedObjectAttribute | SavedObjectAttribute[]; +} + +/** + * + * @public + */ +export interface SavedObject { + /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ + id: string; + /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ + type: string; + /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ + version?: string; + /** Timestamp of the last time this document had been updated. */ + updated_at?: string; + error?: { + message: string; + statusCode: number; + }; + /** {@inheritdoc SavedObjectAttributes} */ + attributes: T; + /** {@inheritdoc SavedObjectReference} */ + references: SavedObjectReference[]; + /** {@inheritdoc SavedObjectsMigrationVersion} */ + migrationVersion?: SavedObjectsMigrationVersion; +} + +/** + * A reference to another saved object. + * + * @public + */ +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} + +/** + * + * @public + */ +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { + type?: string | string[]; + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + /** + * An array of fields to include in the results + * @example + * SavedObjects.find({type: 'dashboard', fields: ['attributes.name', 'attributes.location']}) + */ + fields?: string[]; + /** Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information */ + search?: string; + /** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */ + searchFields?: string[]; + hasReference?: { type: string; id: string }; + defaultSearchOperator?: 'AND' | 'OR'; +} + +/** + * + * @public + */ +export interface SavedObjectsBaseOptions { + /** Specify the namespace for this operation */ + namespace?: string; +} + +/** + * Saved Objects is Kibana's data persisentence mechanism allowing plugins to + * use Elasticsearch for storing plugin state. + * + * ## SavedObjectsClient errors + * + * Since the SavedObjectsClient has its hands in everything we + * are a little paranoid about the way we present errors back to + * to application code. Ideally, all errors will be either: + * + * 1. Caused by bad implementation (ie. undefined is not a function) and + * as such unpredictable + * 2. An error that has been classified and decorated appropriately + * by the decorators in {@link SavedObjectsErrorHelpers} + * + * Type 1 errors are inevitable, but since all expected/handle-able errors + * should be Type 2 the `isXYZError()` helpers exposed at + * `SavedObjectsErrorHelpers` should be used to understand and manage error + * responses from the `SavedObjectsClient`. + * + * Type 2 errors are decorated versions of the source error, so if + * the elasticsearch client threw an error it will be decorated based + * on its type. That means that rather than looking for `error.body.error.type` or + * doing substring checks on `error.body.error.reason`, just use the helpers to + * understand the meaning of the error: + * + * ```js + * if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + * // handle 404 + * } + * + * if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { + * // 401 handling should be automatic, but in case you wanted to know + * } + * + * // always rethrow the error unless you handle it + * throw error; + * ``` + * + * ### 404s from missing index + * + * From the perspective of application code and APIs the SavedObjectsClient is + * a black box that persists objects. One of the internal details that users have + * no control over is that we use an elasticsearch index for persistance and that + * index might be missing. + * + * At the time of writing we are in the process of transitioning away from the + * operating assumption that the SavedObjects index is always available. Part of + * this transition is handling errors resulting from an index missing. These used + * to trigger a 500 error in most cases, and in others cause 404s with different + * error messages. + * + * From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The + * object the request/call was targeting could not be found. This is why #14141 + * takes special care to ensure that 404 errors are generic and don't distinguish + * between index missing or document missing. + * + * ### 503s from missing index + * + * Unlike all other methods, create requests are supposed to succeed even when + * the Kibana index does not exist because it will be automatically created by + * elasticsearch. When that is not the case it is because Elasticsearch's + * `action.auto_create_index` setting prevents it from being created automatically + * so we throw a special 503 with the intention of informing the user that their + * Elasticsearch settings need to be updated. + * + * See {@link SavedObjectsErrorHelpers} + * + * @public + */ +export type SavedObjectsClientContract = Pick; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 0fefb2d80892..0cda3fd5d403 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -5,17 +5,20 @@ ```ts import Boom from 'boom'; -import { ByteSizeValue } from '@kbn/config-schema'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { ConfigOptions } from 'elasticsearch'; +import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; +import { IncomingHttpHeaders } from 'http'; import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; +import { PeerCertificate } from 'tls'; +import { Readable } from 'stream'; import { Request } from 'hapi'; import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; -import { Schema } from '@kbn/config-schema'; import { Server } from 'hapi'; +import { Stream } from 'stream'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Url } from 'url'; @@ -24,26 +27,31 @@ import { Url } from 'url'; export type APICaller = (endpoint: string, clientParams: Record, options?: CallAPIOptions) => Promise; // Warning: (ae-forgotten-export) The symbol "AuthResult" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type AuthenticationHandler = (request: KibanaRequest, t: AuthToolkit) => AuthResult | Promise; +export type AuthenticationHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: AuthToolkit) => AuthResult | KibanaResponse | Promise; // @public -export type AuthHeaders = Record; +export type AuthHeaders = Record; // @public -export interface AuthResultData { - headers: AuthHeaders; - state: Record; +export interface AuthResultParams { + requestHeaders?: AuthHeaders; + responseHeaders?: AuthHeaders; + state?: Record; +} + +// @public +export enum AuthStatus { + authenticated = "authenticated", + unauthenticated = "unauthenticated", + unknown = "unknown" } // @public export interface AuthToolkit { - authenticated: (data?: Partial) => AuthResult; - redirected: (url: string) => AuthResult; - rejected: (error: Error, options?: { - statusCode?: number; - }) => AuthResult; + authenticated: (data?: AuthResultParams) => AuthResult; } // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts @@ -65,6 +73,9 @@ export class ClusterClient { close(): void; } +// @public (undocumented) +export type ConfigPath = string | string[]; + // @internal (undocumented) export class ConfigService { // Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts @@ -79,12 +90,27 @@ export class ConfigService { // (undocumented) isEnabledAtPath(path: ConfigPath): Promise; optionalAtPath(path: ConfigPath): Observable; - // Warning: (ae-forgotten-export) The symbol "ConfigPath" needs to be exported by the entry point index.d.ts setSchema(path: ConfigPath, schema: Type): Promise; } +// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "kibana" does not have an export "IContextContainer" +// +// @public (undocumented) +export interface ContextSetup { + // Warning: (ae-forgotten-export) The symbol "IContextContainer" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "IContextContainer" + createContextContainer(): IContextContainer; +} + +// @internal (undocumented) +export type CoreId = symbol; + // @public export interface CoreSetup { + // (undocumented) + context: { + createContextContainer: ContextSetup['createContextContainer']; + }; // (undocumented) elasticsearch: { adminClient$: Observable; @@ -98,7 +124,6 @@ export interface CoreSetup { registerAuth: HttpServiceSetup['registerAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; basePath: HttpServiceSetup['basePath']; - createNewServer: HttpServiceSetup['createNewServer']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; }; } @@ -107,6 +132,12 @@ export interface CoreSetup { export interface CoreStart { } +// @public +export interface CustomHttpResponseOptions extends HttpResponseOptions { + // (undocumented) + statusCode: number; +} + // @public export interface DiscoveredPlugin { readonly configPath: ConfigPath; @@ -160,26 +191,72 @@ export interface FakeRequest { } // @public -export type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; +export type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined; -// @public (undocumented) -export type Headers = Record; +// @public +export type GetAuthState = (request: KibanaRequest | LegacyRequest) => { + status: AuthStatus; + state: unknown; +}; -// Warning: (ae-forgotten-export) The symbol "HttpServerSetup" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export interface HttpServiceSetup extends HttpServerSetup { - // Warning: (ae-forgotten-export) The symbol "HttpConfig" needs to be exported by the entry point index.d.ts - // +// @public +export type Headers = { + [header in KnownHeaders]?: string | string[] | undefined; +} & { + [header: string]: string | string[] | undefined; +}; + +// @public +export interface HttpResponseOptions { + // Warning: (ae-forgotten-export) The symbol "ResponseHeaders" needs to be exported by the entry point index.d.ts + headers?: ResponseHeaders; +} + +// @public +export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; + +// @public +export interface HttpServerSetup { + // (undocumented) + auth: { + get: GetAuthState; + isAuthenticated: IsAuthenticated; + getAuthHeaders: GetAuthHeaders; + }; // (undocumented) - createNewServer: (cfg: Partial) => Promise; + basePath: { + get: (request: KibanaRequest | LegacyRequest) => string; + set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; + prepend: (url: string) => string; + remove: (url: string) => string; + }; + createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>; + isTlsEnabled: boolean; + registerAuth: (handler: AuthenticationHandler) => void; + registerOnPostAuth: (handler: OnPostAuthHandler) => void; + registerOnPreAuth: (handler: OnPreAuthHandler) => void; + registerRouter: (router: Router) => void; + // (undocumented) + server: Server; } +// @public (undocumented) +export type HttpServiceSetup = HttpServerSetup; + // @public (undocumented) export interface HttpServiceStart { isListening: (port: number) => boolean; } +// @public +export interface IKibanaSocket { + // (undocumented) + getPeerCertificate(detailed: true): DetailedPeerCertificate | null; + // (undocumented) + getPeerCertificate(detailed: false): PeerCertificate | null; + getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; +} + // @internal (undocumented) export interface InternalCoreSetup { // (undocumented) @@ -196,6 +273,9 @@ export interface InternalCoreStart { plugins: PluginsServiceStart; } +// @public +export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; + // @public export class KibanaRequest { // @internal (undocumented) @@ -212,9 +292,9 @@ export class KibanaRequest { readonly params: Params; // (undocumented) readonly query: Query; - // (undocumented) readonly route: RecursiveReadonly; // (undocumented) + readonly socket: IKibanaSocket; readonly url: Url; } @@ -229,7 +309,43 @@ export interface KibanaRequestRoute { } // @public -export type LegacyRequest = Request; +export type KibanaResponseFactory = typeof kibanaResponseFactory; + +// @public +export const kibanaResponseFactory: { + custom: (payload: string | Error | Record | Buffer | Stream | { + message: string | Error; + meta?: ResponseErrorMeta | undefined; + } | undefined, options: CustomHttpResponseOptions) => KibanaResponse | Buffer | Stream | { + message: string | Error; + meta?: ResponseErrorMeta | undefined; + }>; + badRequest: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + unauthorized: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + forbidden: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + notFound: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + conflict: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + internalError: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse; + customError: (error: ResponseError, options: CustomHttpResponseOptions) => KibanaResponse; + redirected: (payload: HttpResponsePayload, options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; + ok: (payload: HttpResponsePayload, options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; + accepted: (payload?: HttpResponsePayload, options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; + noContent: (options?: HttpResponseOptions) => KibanaResponse; +}; + +// Warning: (ae-forgotten-export) The symbol "KnownKeys" needs to be exported by the entry point index.d.ts +// +// @public +export type KnownHeaders = KnownKeys; + +// @public @deprecated (undocumented) +export interface LegacyRequest extends Request { +} + +// Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts +// +// @public +export type LifecycleResponseFactory = typeof lifecycleResponseFactory; // @public export interface Logger { @@ -303,31 +419,22 @@ export interface LogRecord { // Warning: (ae-forgotten-export) The symbol "OnPostAuthResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type OnPostAuthHandler = (request: KibanaRequest, t: OnPostAuthToolkit) => OnPostAuthResult | Promise; +export type OnPostAuthHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPostAuthToolkit) => OnPostAuthResult | KibanaResponse | Promise; // @public export interface OnPostAuthToolkit { next: () => OnPostAuthResult; - redirected: (url: string) => OnPostAuthResult; - rejected: (error: Error, options?: { - statusCode?: number; - }) => OnPostAuthResult; } // Warning: (ae-forgotten-export) The symbol "OnPreAuthResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type OnPreAuthHandler = (request: KibanaRequest, t: OnPreAuthToolkit) => OnPreAuthResult | Promise; +export type OnPreAuthHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPreAuthToolkit) => OnPreAuthResult | KibanaResponse | Promise; // @public export interface OnPreAuthToolkit { next: () => OnPreAuthResult; - redirected: (url: string, options?: { - forward: boolean; - }) => OnPreAuthResult; - rejected: (error: Error, options?: { - statusCode?: number; - }) => OnPreAuthResult; + rewriteUrl: (url: string) => OnPreAuthResult; } // @public @@ -356,11 +463,28 @@ export interface PluginInitializerContext { }; // (undocumented) logger: LoggerFactory; + // (undocumented) + opaqueId: PluginOpaqueId; +} + +// @public +export interface PluginManifest { + readonly configPath: ConfigPath; + readonly id: PluginName; + readonly kibanaVersion: string; + readonly optionalPlugins: readonly PluginName[]; + readonly requiredPlugins: readonly PluginName[]; + readonly server: boolean; + readonly ui: boolean; + readonly version: string; } // @public export type PluginName = string; +// @public (undocumented) +export type PluginOpaqueId = symbol; + // @public (undocumented) export interface PluginsServiceSetup { // (undocumented) @@ -385,6 +509,39 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext [K in keyof T]: RecursiveReadonly; }> : T; +// @public +export type RedirectResponseOptions = HttpResponseOptions & { + headers: { + location: string; + }; +}; + +// @public +export type RequestHandler

    = (request: KibanaRequest, TypeOf, TypeOf>, response: KibanaResponseFactory) => KibanaResponse | Promise>; + +// @public +export type ResponseError = string | Error | { + message: string | Error; + meta?: ResponseErrorMeta; +}; + +// @public +export interface ResponseErrorMeta { + // (undocumented) + data?: Record; + // (undocumented) + docLink?: string; + // (undocumented) + errorCode?: string; +} + +// @public +export interface RouteConfig

    { + options?: RouteConfigOptions; + path: string; + validate: RouteSchemas | false; +} + // @public export interface RouteConfigOptions { authRequired?: boolean; @@ -394,51 +551,42 @@ export interface RouteConfigOptions { // @public export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; -// @public (undocumented) +// @public export class Router { constructor(path: string); delete

    (route: RouteConfig, handler: RequestHandler): void; - // Warning: (ae-forgotten-export) The symbol "RouteConfig" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "RequestHandler" needs to be exported by the entry point index.d.ts get

    (route: RouteConfig, handler: RequestHandler): void; + // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts + // + // @internal getRoutes(): Readonly[]; // (undocumented) readonly path: string; post

    (route: RouteConfig, handler: RequestHandler): void; put

    (route: RouteConfig, handler: RequestHandler): void; - // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts - // - // (undocumented) - routes: Array>; } // @public (undocumented) export interface SavedObject { - // (undocumented) attributes: T; // (undocumented) error?: { message: string; statusCode: number; }; - // (undocumented) id: string; - // (undocumented) migrationVersion?: SavedObjectsMigrationVersion; - // (undocumented) references: SavedObjectReference[]; - // (undocumented) type: string; - // (undocumented) updated_at?: string; - // (undocumented) version?: string; } // @public (undocumented) +export type SavedObjectAttribute = string | number | boolean | null | undefined | SavedObjectAttributes | SavedObjectAttributes[]; + +// @public export interface SavedObjectAttributes { - // Warning: (ae-forgotten-export) The symbol "SavedObjectAttribute" needs to be exported by the entry point index.d.ts - // // (undocumented) [key: string]: SavedObjectAttribute | SavedObjectAttribute[]; } @@ -464,7 +612,6 @@ export interface SavedObjectsBulkCreateObject; -// Warning: (ae-missing-release-tag) "SavedObjectsClientWrapperFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export interface SavedObjectsClientProviderOptions { + // (undocumented) + excludedWrappers?: string[]; +} + +// @public export type SavedObjectsClientWrapperFactory = (options: SavedObjectsClientWrapperOptions) => SavedObjectsClientContract; -// Warning: (ae-missing-release-tag) "SavedObjectsClientWrapperOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface SavedObjectsClientWrapperOptions { // (undocumented) client: SavedObjectsClientContract; @@ -533,7 +682,6 @@ export interface SavedObjectsClientWrapperOptions { // @public (undocumented) export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { id?: string; - // (undocumented) migrationVersion?: SavedObjectsMigrationVersion; overwrite?: boolean; // (undocumented) @@ -590,11 +738,29 @@ export class SavedObjectsErrorHelpers { static isSavedObjectsClientError(error: any): error is DecoratedError; } +// @public +export interface SavedObjectsExportOptions { + // (undocumented) + exportSizeLimit: number; + // (undocumented) + includeReferencesDeep?: boolean; + // (undocumented) + namespace?: string; + // (undocumented) + objects?: Array<{ + id: string; + type: string; + }>; + // (undocumented) + savedObjectsClient: SavedObjectsClientContract; + // (undocumented) + types?: string[]; +} + // @public (undocumented) export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { // (undocumented) defaultSearchOperator?: 'AND' | 'OR'; - // (undocumented) fields?: string[]; // (undocumented) hasReference?: { @@ -605,7 +771,6 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { page?: number; // (undocumented) perPage?: number; - // (undocumented) search?: string; searchFields?: string[]; // (undocumented) @@ -616,7 +781,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { type?: string | string[]; } -// @public (undocumented) +// @public export interface SavedObjectsFindResponse { // (undocumented) page: number; @@ -628,12 +793,174 @@ export interface SavedObjectsFindResponse total: number; } +// @public +export interface SavedObjectsImportConflictError { + // (undocumented) + type: 'conflict'; +} + +// @public +export interface SavedObjectsImportError { + // (undocumented) + error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; + // (undocumented) + id: string; + // (undocumented) + title?: string; + // (undocumented) + type: string; +} + +// @public +export interface SavedObjectsImportMissingReferencesError { + // (undocumented) + blocking: Array<{ + type: string; + id: string; + }>; + // (undocumented) + references: Array<{ + type: string; + id: string; + }>; + // (undocumented) + type: 'missing_references'; +} + +// @public +export interface SavedObjectsImportOptions { + // (undocumented) + namespace?: string; + // (undocumented) + objectLimit: number; + // (undocumented) + overwrite: boolean; + // (undocumented) + readStream: Readable; + // (undocumented) + savedObjectsClient: SavedObjectsClientContract; + // (undocumented) + supportedTypes: string[]; +} + +// @public +export interface SavedObjectsImportResponse { + // (undocumented) + errors?: SavedObjectsImportError[]; + // (undocumented) + success: boolean; + // (undocumented) + successCount: number; +} + +// @public +export interface SavedObjectsImportRetry { + // (undocumented) + id: string; + // (undocumented) + overwrite: boolean; + // (undocumented) + replaceReferences: Array<{ + type: string; + from: string; + to: string; + }>; + // (undocumented) + type: string; +} + +// @public +export interface SavedObjectsImportUnknownError { + // (undocumented) + message: string; + // (undocumented) + statusCode: number; + // (undocumented) + type: 'unknown'; +} + +// @public +export interface SavedObjectsImportUnsupportedTypeError { + // (undocumented) + type: 'unsupported_type'; +} + +// @public (undocumented) +export interface SavedObjectsMigrationLogger { + // (undocumented) + debug: (msg: string) => void; + // (undocumented) + info: (msg: string) => void; + // (undocumented) + warning: (msg: string) => void; +} + // @public export interface SavedObjectsMigrationVersion { // (undocumented) [pluginName: string]: string; } +// Warning: (ae-missing-release-tag) "RawDoc" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface SavedObjectsRawDoc { + // (undocumented) + _id: string; + // (undocumented) + _primary_term?: number; + // (undocumented) + _seq_no?: number; + // (undocumented) + _source: any; + // (undocumented) + _type?: string; +} + +// @public +export interface SavedObjectsResolveImportErrorsOptions { + // (undocumented) + namespace?: string; + // (undocumented) + objectLimit: number; + // (undocumented) + readStream: Readable; + // (undocumented) + retries: SavedObjectsImportRetry[]; + // (undocumented) + savedObjectsClient: SavedObjectsClientContract; + // (undocumented) + supportedTypes: string[]; +} + +// Warning: (ae-missing-release-tag) "SavedObjectsSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class SavedObjectsSchema { + // Warning: (ae-forgotten-export) The symbol "SavedObjectsSchemaDefinition" needs to be exported by the entry point index.d.ts + constructor(schemaDefinition?: SavedObjectsSchemaDefinition); + // (undocumented) + getConvertToAliasScript(type: string): string | undefined; + // (undocumented) + getIndexForType(config: Config, type: string): string | undefined; + // (undocumented) + isHiddenType(type: string): boolean; + // (undocumented) + isNamespaceAgnostic(type: string): boolean; +} + +// Warning: (ae-missing-release-tag) "SavedObjectsSerializer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class SavedObjectsSerializer { + constructor(schema: SavedObjectsSchema); + generateRawId(namespace: string | undefined, type: string, id?: string): string; + isRawSavedObject(rawDoc: SavedObjectsRawDoc): any; + // Warning: (ae-forgotten-export) The symbol "SanitizedSavedObjectDoc" needs to be exported by the entry point index.d.ts + rawToSavedObject(doc: SavedObjectsRawDoc): SanitizedSavedObjectDoc; + savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): SavedObjectsRawDoc; + } + // @public (undocumented) export interface SavedObjectsService { // Warning: (ae-forgotten-export) The symbol "ScopedSavedObjectsClientProvider" needs to be exported by the entry point index.d.ts @@ -644,11 +971,20 @@ export interface SavedObjectsService { getSavedObjectsRepository(...rest: any[]): any; // (undocumented) getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; + // (undocumented) + importExport: { + objectLimit: number; + importSavedObjects(options: SavedObjectsImportOptions): Promise; + resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise; + getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; + }; // Warning: (ae-incompatible-release-tags) The symbol "SavedObjectsClient" is marked as @public, but its signature references "SavedObjectsClient" which is marked as @internal // // (undocumented) SavedObjectsClient: typeof SavedObjectsClient; // (undocumented) + schema: SavedObjectsSchema; + // (undocumented) types: string[]; } @@ -669,7 +1005,7 @@ export interface SavedObjectsUpdateResponse | undefined); + constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: Headers | undefined); callAsCurrentUser(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise; callAsInternalUser(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise; } @@ -681,6 +1017,14 @@ export interface SessionStorage { set(sessionValue: T): void; } +// @public +export interface SessionStorageCookieOptions { + encryptionKey: string; + isSecure: boolean; + name: string; + validate: (sessionValue: T) => boolean | Promise; +} + // @public export interface SessionStorageFactory { // (undocumented) @@ -690,7 +1034,7 @@ export interface SessionStorageFactory { // Warnings were encountered during analysis: // -// src/core/server/plugins/plugin_context.ts:34:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugins_service.ts:37:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugins_service.ts:39:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:162:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 01f2673c3f9e..4c7f1b6ad1a5 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -31,9 +31,11 @@ import { config as httpConfig } from './http'; import { config as loggingConfig } from './logging'; import { config as devConfig } from './dev'; import { mapToObject } from '../utils/'; +import { ContextService } from './context'; export class Server { public readonly configService: ConfigService; + private readonly context: ContextService; private readonly elasticsearch: ElasticsearchService; private readonly http: HttpService; private readonly plugins: PluginsService; @@ -48,7 +50,8 @@ export class Server { this.log = this.logger.get('server'); this.configService = new ConfigService(config$, env, logger); - const core = { configService: this.configService, env, logger }; + const core = { coreId: Symbol('core'), configService: this.configService, env, logger }; + this.context = new ContextService(core); this.http = new HttpService(core); this.plugins = new PluginsService(core); this.legacy = new LegacyService(core); @@ -58,19 +61,25 @@ export class Server { public async setup() { this.log.debug('setting up server'); + // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. + const pluginDependencies = await this.plugins.discover(); + const httpSetup = await this.http.setup(); this.registerDefaultRoute(httpSetup); + const contextServiceSetup = this.context.setup({ pluginDependencies }); const elasticsearchServiceSetup = await this.elasticsearch.setup({ http: httpSetup, }); const pluginsSetup = await this.plugins.setup({ + context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, }); const coreSetup = { + context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, plugins: pluginsSetup, diff --git a/src/core/server/types.ts b/src/core/server/types.ts new file mode 100644 index 000000000000..d712c804d45d --- /dev/null +++ b/src/core/server/types.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** This module is intended for consumption by public to avoid import issues with server-side code */ +export { PluginOpaqueId } from './plugins/types'; +export * from './saved_objects/types'; diff --git a/src/core/server/types.ts~master b/src/core/server/types.ts~master new file mode 100644 index 000000000000..9b55da17a40a --- /dev/null +++ b/src/core/server/types.ts~master @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** This module is intended for consumption by public to avoid import issues with server-side code */ + +export { PluginOpaqueId } from './plugins/types'; diff --git a/src/core/utils/context.mock.ts b/src/core/utils/context.mock.ts new file mode 100644 index 000000000000..a3849fb77f83 --- /dev/null +++ b/src/core/utils/context.mock.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { IContextContainer } from './context'; + +export type ContextContainerMock = jest.Mocked>; + +const createContextMock = () => { + const contextMock: ContextContainerMock = { + registerContext: jest.fn(), + createHandler: jest.fn(), + }; + return contextMock; +}; + +export const contextMock = { + create: createContextMock, +}; diff --git a/src/core/utils/context.test.ts b/src/core/utils/context.test.ts new file mode 100644 index 000000000000..78d067388853 --- /dev/null +++ b/src/core/utils/context.test.ts @@ -0,0 +1,232 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ContextContainer } from './context'; +import { PluginOpaqueId } from '../server'; + +const pluginA = Symbol('pluginA'); +const pluginB = Symbol('pluginB'); +const pluginC = Symbol('pluginC'); +const pluginD = Symbol('pluginD'); +const plugins: ReadonlyMap = new Map([ + [pluginA, []], + [pluginB, [pluginA]], + [pluginC, [pluginA, pluginB]], + [pluginD, []], +]); + +interface MyContext { + core1: string; + core2: number; + ctxFromA: string; + ctxFromB: number; + ctxFromC: boolean; + ctxFromD: object; +} + +const coreId = Symbol(); + +describe('ContextContainer', () => { + it('does not allow the same context to be registered twice', () => { + const contextContainer = new ContextContainer(plugins, coreId); + contextContainer.registerContext(coreId, 'ctxFromA', () => 'aString'); + + expect(() => + contextContainer.registerContext(coreId, 'ctxFromA', () => 'aString') + ).toThrowErrorMatchingInlineSnapshot( + `"Context provider for ctxFromA has already been registered."` + ); + }); + + describe('registerContext', () => { + it('throws error if called with an unknown symbol', async () => { + const contextContainer = new ContextContainer(plugins, coreId); + await expect(() => + contextContainer.registerContext(Symbol('unknown'), 'ctxFromA', jest.fn()) + ).toThrowErrorMatchingInlineSnapshot( + `"Cannot register context for unknown plugin: Symbol(unknown)"` + ); + }); + }); + + describe('context building', () => { + it('resolves dependencies', async () => { + const contextContainer = new ContextContainer(plugins, coreId); + expect.assertions(8); + contextContainer.registerContext(coreId, 'core1', context => { + expect(context).toEqual({}); + return 'core'; + }); + + contextContainer.registerContext(pluginA, 'ctxFromA', context => { + expect(context).toEqual({ core1: 'core' }); + return 'aString'; + }); + contextContainer.registerContext(pluginB, 'ctxFromB', context => { + expect(context).toEqual({ core1: 'core', ctxFromA: 'aString' }); + return 299; + }); + contextContainer.registerContext(pluginC, 'ctxFromC', context => { + expect(context).toEqual({ core1: 'core', ctxFromA: 'aString', ctxFromB: 299 }); + return false; + }); + contextContainer.registerContext(pluginD, 'ctxFromD', context => { + expect(context).toEqual({ core1: 'core' }); + return {}; + }); + + const rawHandler1 = jest.fn(() => 'handler1'); + const handler1 = contextContainer.createHandler(pluginC, rawHandler1); + + const rawHandler2 = jest.fn(() => 'handler2'); + const handler2 = contextContainer.createHandler(pluginD, rawHandler2); + + await handler1(); + await handler2(); + + // Should have context from pluginC, its deps, and core + expect(rawHandler1).toHaveBeenCalledWith({ + core1: 'core', + ctxFromA: 'aString', + ctxFromB: 299, + ctxFromC: false, + }); + + // Should have context from pluginD, and core + expect(rawHandler2).toHaveBeenCalledWith({ + core1: 'core', + ctxFromD: {}, + }); + }); + + it('exposes all core context to core providers', async () => { + expect.assertions(4); + const contextContainer = new ContextContainer(plugins, coreId); + + contextContainer + .registerContext(coreId, 'core1', context => { + expect(context).toEqual({}); + return 'core'; + }) + .registerContext(coreId, 'core2', context => { + expect(context).toEqual({ core1: 'core' }); + return 101; + }); + + const rawHandler1 = jest.fn(() => 'handler1'); + const handler1 = contextContainer.createHandler(pluginA, rawHandler1); + + expect(await handler1()).toEqual('handler1'); + + // If no context is registered for pluginA, only core contexts should be exposed + expect(rawHandler1).toHaveBeenCalledWith({ + core1: 'core', + core2: 101, + }); + }); + + it('does not expose plugin contexts to core handler', async () => { + const contextContainer = new ContextContainer(plugins, coreId); + + contextContainer + .registerContext(coreId, 'core1', context => 'core') + .registerContext(pluginA, 'ctxFromA', context => 'aString'); + + const rawHandler1 = jest.fn(() => 'handler1'); + const handler1 = contextContainer.createHandler(coreId, rawHandler1); + + expect(await handler1()).toEqual('handler1'); + // pluginA context should not be present in a core handler + expect(rawHandler1).toHaveBeenCalledWith({ + core1: 'core', + }); + }); + + it('passes additional arguments to providers', async () => { + expect.assertions(6); + const contextContainer = new ContextContainer( + plugins, + coreId + ); + + contextContainer.registerContext(coreId, 'core1', (context, str, num) => { + expect(str).toEqual('passed string'); + expect(num).toEqual(77); + return `core ${str}`; + }); + + contextContainer.registerContext(pluginD, 'ctxFromD', (context, str, num) => { + expect(str).toEqual('passed string'); + expect(num).toEqual(77); + return { + num: 77, + }; + }); + + const rawHandler1 = jest.fn(() => 'handler1'); + const handler1 = contextContainer.createHandler(pluginD, rawHandler1); + + expect(await handler1('passed string', 77)).toEqual('handler1'); + + expect(rawHandler1).toHaveBeenCalledWith( + { + core1: 'core passed string', + ctxFromD: { + num: 77, + }, + }, + 'passed string', + 77 + ); + }); + }); + + describe('createHandler', () => { + it('throws error if called with an unknown symbol', async () => { + const contextContainer = new ContextContainer(plugins, coreId); + await expect(() => + contextContainer.createHandler(Symbol('unknown'), jest.fn()) + ).toThrowErrorMatchingInlineSnapshot( + `"Cannot create handler for unknown plugin: Symbol(unknown)"` + ); + }); + + it('returns value from original handler', async () => { + const contextContainer = new ContextContainer(plugins, coreId); + + const rawHandler1 = jest.fn(() => 'handler1'); + const handler1 = contextContainer.createHandler(pluginA, rawHandler1); + + expect(await handler1()).toEqual('handler1'); + }); + + it('passes additional arguments to handlers', async () => { + const contextContainer = new ContextContainer( + plugins, + coreId + ); + + const rawHandler1 = jest.fn(() => 'handler1'); + const handler1 = contextContainer.createHandler(pluginA, rawHandler1); + + await handler1('passed string', 77); + expect(rawHandler1).toHaveBeenCalledWith({}, 'passed string', 77); + }); + }); +}); diff --git a/src/core/utils/context.ts b/src/core/utils/context.ts new file mode 100644 index 000000000000..2efe68cc5d76 --- /dev/null +++ b/src/core/utils/context.ts @@ -0,0 +1,292 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { flatten } from 'lodash'; +import { pick } from '.'; +import { CoreId, PluginOpaqueId } from '../server'; + +/** + * A function that returns a context value for a specific key of given context type. + * + * @remarks + * This function will be called each time a new context is built for a handler invocation. + * + * @param context - A partial context object containing only the keys for values provided by plugin dependencies + * @param rest - Additional parameters provided by the service owner of this context + * @returns The context value associated with this key. May also return a Promise which will be resolved before + * attaching to the context object. + * + * @public + */ +export type IContextProvider< + TContext extends Record, + TContextName extends keyof TContext, + TProviderParameters extends any[] = [] +> = ( + context: Partial, + ...rest: TProviderParameters +) => Promise | TContext[TContextName]; + +/** + * A function registered by a plugin to perform some action. + * + * @remarks + * A new `TContext` will be built for each handler before invoking. + * + * @public + */ +export type IContextHandler = ( + context: TContext, + ...rest: THandlerParameters +) => TReturn; + +type Promisify = T extends Promise ? Promise : Promise; + +/** + * An object that handles registration of context providers and configuring handlers with context. + * + * @remarks + * A {@link IContextContainer} can be used by any Core service or plugin (known as the "service owner") which wishes to + * expose APIs in a handler function. The container object will manage registering context providers and configuring a + * handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the + * dependencies that the handler's plugin declares. + * + * Contexts providers are executed in the order they were registered. Each provider gets access to context values + * provided by any plugins that it depends on. + * + * In order to configure a handler with context, you must call the {@link IContextContainer.createHandler} function and + * use the returned handler which will automatically build a context object when called. + * + * When registering context or creating handlers, the _calling plugin's opaque id_ must be provided. This id is passed + * in via the plugin's initializer and can be accessed from the {@link PluginInitializerContext.opaqueId} Note this + * should NOT be the context service owner's id, but the plugin that is actually registering the context or handler. + * + * ```ts + * // Correct + * class MyPlugin { + * private readonly handlers = new Map(); + * + * setup(core) { + * this.contextContainer = core.context.createContextContainer(); + * return { + * registerContext(pluginOpaqueId, contextName, provider) { + * this.contextContainer.registerContext(pluginOpaqueId, contextName, provider); + * }, + * registerRoute(pluginOpaqueId, path, handler) { + * this.handlers.set( + * path, + * this.contextContainer.createHandler(pluginOpaqueId, handler) + * ); + * } + * } + * } + * } + * + * // Incorrect + * class MyPlugin { + * private readonly handlers = new Map(); + * + * constructor(private readonly initContext: PluginInitializerContext) {} + * + * setup(core) { + * this.contextContainer = core.context.createContextContainer(); + * return { + * registerContext(contextName, provider) { + * // BUG! + * // This would leak this context to all handlers rather that only plugins that depend on the calling plugin. + * this.contextContainer.registerContext(this.initContext.opaqueId, contextName, provider); + * }, + * registerRoute(path, handler) { + * this.handlers.set( + * path, + * // BUG! + * // This handler will not receive any contexts provided by other dependencies of the calling plugin. + * this.contextContainer.createHandler(this.initContext.opaqueId, handler) + * ); + * } + * } + * } + * } + * ``` + * + * @public + */ +export interface IContextContainer< + TContext extends {}, + THandlerReturn, + THandlerParameters extends any[] = [] +> { + /** + * Register a new context provider. + * + * @remarks + * The value (or resolved Promise value) returned by the `provider` function will be attached to the context object + * on the key specified by `contextName`. + * + * Throws an exception if more than one provider is registered for the same `contextName`. + * + * @param pluginOpaqueId - The plugin opaque ID for the plugin that registers this context. + * @param contextName - The key of the `TContext` object this provider supplies the value for. + * @param provider - A {@link IContextProvider} to be called each time a new context is created. + * @returns The {@link IContextContainer} for method chaining. + */ + registerContext( + pluginOpaqueId: PluginOpaqueId, + contextName: TContextName, + provider: IContextProvider + ): this; + + /** + * Create a new handler function pre-wired to context for the plugin. + * + * @param pluginOpaqueId - The plugin opaque ID for the plugin that registers this handler. + * @param handler - Handler function to pass context object to. + * @returns A function that takes `THandlerParameters`, calls `handler` with a new context, and returns a Promise of + * the `handler` return value. + */ + createHandler( + pluginOpaqueId: PluginOpaqueId, + handler: IContextHandler + ): (...rest: THandlerParameters) => Promisify; +} + +/** @internal */ +export class ContextContainer< + TContext extends Record, + THandlerReturn, + THandlerParameters extends any[] = [] +> implements IContextContainer { + /** + * Used to map contexts to their providers and associated plugin. In registration order which is tightly coupled to + * plugin load order. + */ + private readonly contextProviders = new Map< + keyof TContext, + { + provider: IContextProvider; + source: symbol; + } + >(); + /** Used to keep track of which plugins registered which contexts for dependency resolution. */ + private readonly contextNamesBySource: Map>; + + /** + * @param pluginDependencies - A map of plugins to an array of their dependencies. + */ + constructor( + private readonly pluginDependencies: ReadonlyMap, + private readonly coreId: CoreId + ) { + this.contextNamesBySource = new Map>([[coreId, []]]); + } + + public registerContext = ( + source: symbol, + contextName: TContextName, + provider: IContextProvider + ): this => { + if (this.contextProviders.has(contextName)) { + throw new Error(`Context provider for ${contextName} has already been registered.`); + } + if (source !== this.coreId && !this.pluginDependencies.has(source)) { + throw new Error(`Cannot register context for unknown plugin: ${source.toString()}`); + } + + this.contextProviders.set(contextName, { provider, source }); + this.contextNamesBySource.set(source, [ + ...(this.contextNamesBySource.get(source) || []), + contextName, + ]); + + return this; + }; + + public createHandler = ( + source: symbol, + handler: IContextHandler + ) => { + if (source !== this.coreId && !this.pluginDependencies.has(source)) { + throw new Error(`Cannot create handler for unknown plugin: ${source.toString()}`); + } + + return (async (...args: THandlerParameters) => { + const context = await this.buildContext(source, ...args); + return handler(context, ...args); + }) as (...args: THandlerParameters) => Promisify; + }; + + private async buildContext( + source: symbol, + ...contextArgs: THandlerParameters + ): Promise { + const contextsToBuild: ReadonlySet = new Set( + this.getContextNamesForSource(source) + ); + + return [...this.contextProviders] + .filter(([contextName]) => contextsToBuild.has(contextName)) + .reduce( + async (contextPromise, [contextName, { provider, source: providerSource }]) => { + const resolvedContext = await contextPromise; + + // For the next provider, only expose the context available based on the dependencies of the plugin that + // registered that provider. + const exposedContext = pick(resolvedContext, [ + ...this.getContextNamesForSource(providerSource), + ]); + + return { + ...resolvedContext, + [contextName]: await provider(exposedContext as Partial, ...contextArgs), + }; + }, + Promise.resolve({}) as Promise + ); + } + + private getContextNamesForSource(source: symbol): ReadonlySet { + if (source === this.coreId) { + return this.getContextNamesForCore(); + } else { + return this.getContextNamesForPluginId(source); + } + } + + private getContextNamesForCore() { + return new Set(this.contextNamesBySource.get(this.coreId)!); + } + + private getContextNamesForPluginId(pluginId: symbol) { + // If the source is a plugin... + const pluginDeps = this.pluginDependencies.get(pluginId); + if (!pluginDeps) { + // This case should never be hit, but let's be safe. + throw new Error(`Cannot create context for unknown plugin: ${pluginId.toString()}`); + } + + return new Set([ + // Core contexts + ...this.contextNamesBySource.get(this.coreId)!, + // Contexts source created + ...(this.contextNamesBySource.get(pluginId) || []), + // Contexts sources's dependencies created + ...flatten(pluginDeps.map(p => this.contextNamesBySource.get(p) || [])), + ]); + } +} diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index d4391f576df4..b6ffb57db975 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -17,9 +17,10 @@ * under the License. */ +export * from './assert_never'; +export * from './context'; +export * from './deep_freeze'; export * from './get'; export * from './map_to_object'; export * from './pick'; -export * from './assert_never'; export * from './url'; -export * from './deep_freeze'; diff --git a/src/core/utils/map_to_object.ts b/src/core/utils/map_to_object.ts index bfbe5c8ab0be..edb2fc2bcbfc 100644 --- a/src/core/utils/map_to_object.ts +++ b/src/core/utils/map_to_object.ts @@ -17,7 +17,7 @@ * under the License. */ -export function mapToObject(map: Map) { +export function mapToObject(map: ReadonlyMap) { const result: Record = Object.create(null); for (const [key, value] of map) { result[key] = value; diff --git a/src/dev/build/tasks/optimize_task.js b/src/dev/build/tasks/optimize_task.js index f6de0c717abf..bbd8f9622535 100644 --- a/src/dev/build/tasks/optimize_task.js +++ b/src/dev/build/tasks/optimize_task.js @@ -49,7 +49,7 @@ export const OptimizeBuildTask = { env: { FORCE_DLL_CREATION: 'true', KBN_CACHE_LOADER_WRITABLE: 'true', - NODE_OPTIONS: '--max-old-space-size=2048' + NODE_OPTIONS: '--max-old-space-size=3072' }, }); diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index e599fa54593f..1ec359d88197 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -82,6 +82,14 @@ kibana_vars=( vega.enableExternalUrls xpack.apm.enabled xpack.apm.ui.enabled + xpack.apm.ui.maxTraceItems + apm_oss.apmAgentConfigurationIndex + apm_oss.indexPattern + apm_oss.errorIndices + apm_oss.onboardingIndices + apm_oss.spanIndices + apm_oss.transactionIndices + apm_oss.metricsIndices xpack.canvas.enabled xpack.graph.enabled xpack.grokdebugger.enabled diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js index 95b0740ca738..4a3c21aa5e81 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.js @@ -32,7 +32,7 @@ function generator({ artifactTarball, versionTag, license, usePublicArtifact }) # # ** THIS IS AN AUTO-GENERATED FILE ** # - + ################################################################################ # Build stage 0 # Extract Kibana and make various file manipulations. @@ -48,51 +48,60 @@ function generator({ artifactTarball, versionTag, license, usePublicArtifact }) # REF: https://docs.openshift.org/latest/creating_images/guidelines.html RUN chmod -R g=u /usr/share/kibana RUN find /usr/share/kibana -type d -exec chmod g+s {} \\; - + ################################################################################ # Build stage 1 # Copy prepared files from the previous stage and complete the image. ################################################################################ FROM centos:7 EXPOSE 5601 - + # Add Reporting dependencies. RUN yum update -y && yum install -y fontconfig freetype && yum clean all - + + # Add an init process, check the checksum to make sure it's a match + RUN curl -L -o /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64 + RUN echo "37f2c1f0372a45554f1b89924fbb134fc24c3756efaedf11e07f599494e0eff9 /usr/local/bin/dumb-init" | sha256sum -c - + RUN chmod +x /usr/local/bin/dumb-init + + # Bring in Kibana from the initial stage. COPY --from=prep_files --chown=1000:0 /usr/share/kibana /usr/share/kibana WORKDIR /usr/share/kibana RUN ln -s /usr/share/kibana /opt/kibana - + ENV ELASTIC_CONTAINER true ENV PATH=/usr/share/kibana/bin:$PATH - + # Set some Kibana configuration defaults. COPY --chown=1000:0 config/kibana.yml /usr/share/kibana/config/kibana.yml - + # Add the launcher/wrapper script. It knows how to interpret environment # variables and translate them to Kibana CLI options. COPY --chown=1000:0 bin/kibana-docker /usr/local/bin/ - + # Ensure gid 0 write permissions for OpenShift. RUN chmod g+ws /usr/share/kibana && \\ find /usr/share/kibana -gid 0 -and -not -perm /g+w -exec chmod g+w {} \\; - + # Provide a non-root user to run the process. RUN groupadd --gid 1000 kibana && \\ useradd --uid 1000 --gid 1000 \\ --home-dir /usr/share/kibana --no-create-home \\ kibana USER kibana - + LABEL org.label-schema.schema-version="1.0" \\ org.label-schema.vendor="Elastic" \\ org.label-schema.name="kibana" \\ org.label-schema.version="${ versionTag }" \\ org.label-schema.url="https://www.elastic.co/products/kibana" \\ org.label-schema.vcs-url="https://github.com/elastic/kibana" \\ + org.label-schema.license="${ license }" \\ license="${ license }" - + + ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] + CMD ["/usr/local/bin/kibana-docker"] `); } diff --git a/src/dev/ci_setup/load_env_keys.sh b/src/dev/ci_setup/load_env_keys.sh new file mode 100644 index 000000000000..af599e0712f3 --- /dev/null +++ b/src/dev/ci_setup/load_env_keys.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -e + +if [ -z "$VAULT_SECRET_ID" ]; then + echo "" + echo "" + echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"; + echo " VAULT_SECRET_ID not set, not loading tokens into env"; + echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"; + echo "" + echo "" +else + # load shared helpers to get `retry` function + source /usr/local/bin/bash_standard_lib.sh + + set +x + + # export after define to avoid https://github.com/koalaman/shellcheck/wiki/SC2155 + VAULT_TOKEN=$(retry 5 vault write -field=token auth/approle/login role_id="$VAULT_ROLE_ID" secret_id="$VAULT_SECRET_ID") + export VAULT_TOKEN + + # Set GITHUB_TOKEN for reporting test failures + GITHUB_TOKEN=$(retry 5 vault read -field=github_token secret/kibana-issues/dev/kibanamachine) + export GITHUB_TOKEN + + KIBANA_CI_REPORTER_KEY=$(retry 5 vault read -field=value secret/kibana-issues/dev/kibanamachine-reporter) + export KIBANA_CI_REPORTER_KEY + + PERCY_TOKEN=$(retry 5 vault read -field=value secret/kibana-issues/dev/percy) + export PERCY_TOKEN + + # remove vault related secrets + unset VAULT_ROLE_ID VAULT_SECRET_ID VAULT_TOKEN VAULT_ADDR +fi diff --git a/src/dev/constants.ts b/src/dev/constants.ts index 04be32fe03dc..d1b89719c69b 100644 --- a/src/dev/constants.ts +++ b/src/dev/constants.ts @@ -21,6 +21,6 @@ import { dirname } from 'path'; export const REPO_ROOT = dirname(require.resolve('../../package.json')); -// FIles in directories of this name will be treated as Jest integration tests with instances of +// Files in directories of this name will be treated as Jest integration tests with instances of // Elasticsearch and the Kibana server. export const RESERVED_DIR_JEST_INTEGRATION_TESTS = 'integration_tests'; diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index cd800f264957..e694e9142126 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -30,7 +30,7 @@ export default { '/src/cli', '/src/cli_keystore', '/src/cli_plugin', - '/src/functional_test_runner', + '/packages/kbn-test/target/functional_test_runner', '/src/dev', '/src/legacy/utils', '/src/setup_node_env', diff --git a/src/dev/jest/setup/polyfills.js b/src/dev/jest/setup/polyfills.js index 293fbcf3616d..9394de0aea93 100644 --- a/src/dev/jest/setup/polyfills.js +++ b/src/dev/jest/setup/polyfills.js @@ -23,17 +23,6 @@ const bluebird = require('bluebird'); bluebird.Promise.setScheduler(function (fn) { global.setImmediate.call(global, fn); }); const MutationObserver = require('mutation-observer'); -// There's a bug in mutation-observer around the `attributes` option -// https://dom.spec.whatwg.org/#mutationobserver -// If either options's attributeOldValue or attributeFilter is present and options's attributes is omitted, then set options's attributes to true. -const _observe = MutationObserver.prototype.observe; -MutationObserver.prototype.observe = function observe(target, options) { - const needsAttributes = options.hasOwnProperty('attributeOldValue') || options.hasOwnProperty('attributeFilter'); - if (needsAttributes && !options.hasOwnProperty('attributes')) { - options.attributes = true; - } - Function.prototype.call(_observe, this, target, options); -}; Object.defineProperty(window, 'MutationObserver', { value: MutationObserver }); require('whatwg-fetch'); diff --git a/src/dev/mocha/__tests__/junit_report_generation.js b/src/dev/mocha/__tests__/junit_report_generation.js index f7631e972ad1..162431d10ae3 100644 --- a/src/dev/mocha/__tests__/junit_report_generation.js +++ b/src/dev/mocha/__tests__/junit_report_generation.js @@ -42,26 +42,26 @@ describe('dev/mocha/junit report generation', () => { reporter: function Runner(runner) { setupJUnitReportGeneration(runner, { reportName: 'test', - rootDirectory: PROJECT_DIR + rootDirectory: PROJECT_DIR, }); - } + }, }); mocha.addFile(resolve(PROJECT_DIR, 'test.js')); await new Promise(resolve => mocha.run(resolve)); - const report = await fcb(cb => parseString(readFileSync(resolve(PROJECT_DIR, 'target/junit/TEST-test.xml')), cb)); + const report = await fcb(cb => + parseString(readFileSync(resolve(PROJECT_DIR, 'target/junit/TEST-test.xml')), cb) + ); // test case results are wrapped in expect(report).to.eql({ testsuites: { - testsuite: [ - report.testsuites.testsuite[0] - ] - } + testsuite: [report.testsuites.testsuite[0]], + }, }); // the single element at the root contains summary data for all tests results - const [ testsuite ] = report.testsuites.testsuite; + const [testsuite] = report.testsuites.testsuite; expect(testsuite.$.time).to.match(DURATION_REGEX); expect(testsuite.$.timestamp).to.match(ISO_DATE_SEC_REGEX); expect(testsuite).to.eql({ @@ -78,12 +78,7 @@ describe('dev/mocha/junit report generation', () => { // there are actually only three tests, but since the hook failed // it is reported as a test failure expect(testsuite.testcase).to.have.length(4); - const [ - testPass, - testFail, - beforeEachFail, - testSkipped, - ] = testsuite.testcase; + const [testPass, testFail, beforeEachFail, testSkipped] = testsuite.testcase; const sharedClassname = testPass.$.classname; expect(sharedClassname).to.match(/^test\.test[^\.]js$/); @@ -94,7 +89,7 @@ describe('dev/mocha/junit report generation', () => { name: 'SUITE works', time: testPass.$.time, }, - 'system-out': testPass['system-out'] + 'system-out': testPass['system-out'], }); expect(testFail.$.time).to.match(DURATION_REGEX); @@ -106,14 +101,14 @@ describe('dev/mocha/junit report generation', () => { time: testFail.$.time, }, 'system-out': testFail['system-out'], - failure: [ - testFail.failure[0] - ] + failure: [testFail.failure[0]], }); expect(beforeEachFail.$.time).to.match(DURATION_REGEX); expect(beforeEachFail.failure).to.have.length(1); - expect(beforeEachFail.failure[0]).to.match(/Error: FORCE_HOOK_FAIL\n.+fixtures.project.test.js/); + expect(beforeEachFail.failure[0]).to.match( + /Error: FORCE_HOOK_FAIL\n.+fixtures.project.test.js/ + ); expect(beforeEachFail).to.eql({ $: { classname: sharedClassname, @@ -121,9 +116,7 @@ describe('dev/mocha/junit report generation', () => { time: beforeEachFail.$.time, }, 'system-out': testFail['system-out'], - failure: [ - beforeEachFail.failure[0] - ] + failure: [beforeEachFail.failure[0]], }); expect(testSkipped).to.eql({ @@ -132,7 +125,7 @@ describe('dev/mocha/junit report generation', () => { name: 'SUITE SUB_SUITE never runs', }, 'system-out': testFail['system-out'], - skipped: [''] + skipped: [''], }); }); }); diff --git a/src/dev/mocha/junit_report_generation.js b/src/dev/mocha/junit_report_generation.js index 74b271e82f52..2efac9cf12e3 100644 --- a/src/dev/mocha/junit_report_generation.js +++ b/src/dev/mocha/junit_report_generation.js @@ -38,17 +38,13 @@ export function setupJUnitReportGeneration(runner, options = {}) { const stats = {}; const results = []; - const getDuration = (node) => ( - node.startTime && node.endTime - ? ((node.endTime - node.startTime) / 1000).toFixed(3) - : null - ); + const getDuration = node => + node.startTime && node.endTime ? ((node.endTime - node.startTime) / 1000).toFixed(3) : null; - const findAllTests = (suite) => ( - suite.suites.reduce((acc, suite) => acc.concat(findAllTests(suite)), suite.tests) - ); + const findAllTests = suite => + suite.suites.reduce((acc, suite) => acc.concat(findAllTests(suite)), suite.tests); - const setStartTime = (node) => { + const setStartTime = node => { node.startTime = dateNow(); }; @@ -78,7 +74,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { runner.on('hook', setStartTime); runner.on('hook end', setEndTime); runner.on('test', setStartTime); - runner.on('pass', (node) => results.push({ node })); + runner.on('pass', node => results.push({ node })); runner.on('pass', setEndTime); runner.on('fail', (node, error) => results.push({ failed: true, error, node })); runner.on('fail', setEndTime); @@ -128,9 +124,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { [...results, ...skippedResults].forEach(result => { const el = addTestcaseEl(result.node); - el.ele('system-out').dat( - escapeCdata(getSnapshotOfRunnableLogs(result.node) || '') - ); + el.ele('system-out').dat(escapeCdata(getSnapshotOfRunnableLogs(result.node) || '')); if (result.failed) { el.ele('failure').dat(escapeCdata(inspect(result.error))); @@ -147,7 +141,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { pretty: true, indent: ' ', newline: '\n', - spacebeforeslash: '' + spacebeforeslash: '', }); mkdirp.sync(dirname(reportPath)); diff --git a/src/dev/mocha/run_mocha_cli.js b/src/dev/mocha/run_mocha_cli.js index d2bb9ef9e018..f914e2b2499e 100644 --- a/src/dev/mocha/run_mocha_cli.js +++ b/src/dev/mocha/run_mocha_cli.js @@ -27,15 +27,11 @@ export function runMochaCli() { alias: { t: 'timeout', }, - boolean: [ - 'no-timeouts' - ], + boolean: ['no-timeouts'], }); - const runInBand = ( - process.execArgv.includes('--inspect') || - process.execArgv.includes('--inspect-brk') - ); + const runInBand = + process.execArgv.includes('--inspect') || process.execArgv.includes('--inspect-brk'); // ensure that mocha exits when test have completed process.argv.push('--exit'); @@ -69,25 +65,26 @@ export function runMochaCli() { // set default test files if (!opts._.length) { - globby.sync([ - 'src/**/__tests__/**/*.js', - 'packages/elastic-datemath/test/**/*.js', - 'packages/kbn-dev-utils/src/**/__tests__/**/*.js', - 'packages/kbn-es-query/src/**/__tests__/**/*.js', - 'packages/kbn-eslint-plugin-eslint/**/__tests__/**/*.js', - 'tasks/**/__tests__/**/*.js', - ], { - cwd: resolve(__dirname, '../../..'), - onlyFiles: true, - absolute: true, - ignore: [ - '**/__tests__/fixtures/**', - 'src/**/public/**', - '**/_*.js' - ] - }).forEach(file => { - process.argv.push(file); - }); + globby + .sync( + [ + 'src/**/__tests__/**/*.js', + 'packages/elastic-datemath/test/**/*.js', + 'packages/kbn-dev-utils/src/**/__tests__/**/*.js', + 'packages/kbn-es-query/src/**/__tests__/**/*.js', + 'packages/kbn-eslint-plugin-eslint/**/__tests__/**/*.js', + 'tasks/**/__tests__/**/*.js', + ], + { + cwd: resolve(__dirname, '../../..'), + onlyFiles: true, + absolute: true, + ignore: ['**/__tests__/fixtures/**', 'src/**/public/**', '**/_*.js'], + } + ) + .forEach(file => { + process.argv.push(file); + }); } if (runInBand) { diff --git a/src/dev/run_check_core_api_changes.ts b/src/dev/run_check_core_api_changes.ts index f17f6c2e5efb..4d0be7f38846 100644 --- a/src/dev/run_check_core_api_changes.ts +++ b/src/dev/run_check_core_api_changes.ts @@ -39,7 +39,7 @@ const apiExtractorConfig = (folder: string): ExtractorConfig => { tsconfigFilePath: '/tsconfig.json', }, projectFolder: path.resolve('./'), - mainEntryPointFilePath: `target/types/${folder}/index.d.ts`, + mainEntryPointFilePath: `target/types/core/${folder}/index.d.ts`, apiReport: { enabled: true, reportFileName: `${folder}.api.md`, diff --git a/src/docs/docs_repo.js b/src/docs/docs_repo.js index 7c415f58d499..63fcd2a6de5e 100644 --- a/src/docs/docs_repo.js +++ b/src/docs/docs_repo.js @@ -22,7 +22,7 @@ import { resolve } from 'path'; const kibanaDir = resolve(__dirname, '..', '..'); export function buildDocsScript(cmd) { - return resolve(process.cwd(), cmd.docrepo, 'build_docs.pl'); + return resolve(process.cwd(), cmd.docrepo, 'build_docs'); } export function buildDocsArgs(cmd) { diff --git a/src/es_archiver/cli.js b/src/es_archiver/cli.js index f12e452edf88..f37b6afdc4ba 100644 --- a/src/es_archiver/cli.js +++ b/src/es_archiver/cli.js @@ -33,7 +33,7 @@ import elasticsearch from 'elasticsearch'; import { EsArchiver } from './es_archiver'; import { ToolingLog } from '@kbn/dev-utils'; -import { readConfigFile } from '../functional_test_runner'; +import { readConfigFile } from '@kbn/test'; const cmd = new Command('node scripts/es_archiver'); diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index 3cbc5375e71b..335e2acf2393 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -83,6 +83,7 @@ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { 'migrations.scrollDuration': '5m', 'migrations.batchSize': 100, 'migrations.pollInterval': 100, + 'xpack.task_manager.index': '.kibana_task_manager', }; const ready = async () => undefined; const elasticsearch = { diff --git a/src/fixtures/stubbed_saved_object_index_pattern.js b/src/fixtures/stubbed_saved_object_index_pattern.js index a98339132813..e737b2563823 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.js +++ b/src/fixtures/stubbed_saved_object_index_pattern.js @@ -18,7 +18,7 @@ */ import stubbedLogstashFields from './logstash_fields'; -import { SimpleSavedObject } from '../legacy/ui/public/saved_objects/simple_saved_object'; +import { SimpleSavedObject } from '../core/public'; const mockLogstashFields = stubbedLogstashFields(); diff --git a/src/functional_test_runner/cli.ts b/src/functional_test_runner/cli.ts deleted file mode 100644 index 17bde6a50cbf..000000000000 --- a/src/functional_test_runner/cli.ts +++ /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 { resolve } from 'path'; - -import { run } from '../dev/run'; -import { FunctionalTestRunner } from './functional_test_runner'; - -run( - async ({ flags, log }) => { - const resolveConfigPath = (v: string) => resolve(process.cwd(), v); - const toArray = (v: string | string[]) => ([] as string[]).concat(v || []); - - const functionalTestRunner = new FunctionalTestRunner( - log, - resolveConfigPath(flags.config as string), - { - mochaOpts: { - bail: flags.bail, - grep: flags.grep || undefined, - invert: flags.invert, - }, - suiteTags: { - include: toArray(flags['include-tag'] as string | string[]), - exclude: toArray(flags['exclude-tag'] as string | string[]), - }, - updateBaselines: flags.updateBaselines, - excludeTestFiles: flags.exclude || undefined, - } - ); - - let teardownRun = false; - const teardown = async (err?: Error) => { - if (teardownRun) return; - - teardownRun = true; - if (err) { - log.indent(-log.indent()); - log.error(err); - process.exitCode = 1; - } - - try { - await functionalTestRunner.close(); - } finally { - process.exit(); - } - }; - - process.on('unhandledRejection', err => teardown(err)); - process.on('SIGTERM', () => teardown()); - process.on('SIGINT', () => teardown()); - - try { - if (flags['test-stats']) { - process.stderr.write( - JSON.stringify(await functionalTestRunner.getTestStats(), null, 2) + '\n' - ); - } else { - const failureCount = await functionalTestRunner.run(); - process.exitCode = failureCount ? 1 : 0; - } - } catch (err) { - await teardown(err); - } finally { - await teardown(); - } - }, - { - flags: { - string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag'], - boolean: ['bail', 'invert', 'test-stats', 'updateBaselines'], - default: { - config: 'test/functional/config.js', - debug: true, - }, - help: ` - --config=path path to a config file - --bail stop tests after the first failure - --grep pattern used to select which tests to run - --invert invert grep to exclude tests - --exclude=file path to a test file that should not be loaded - --include-tag=tag a tag to be included, pass multiple times for multiple tags - --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags - --test-stats print the number of tests (included and excluded) to STDERR - --updateBaselines replace baseline screenshots with whatever is generated from the test - `, - }, - } -); diff --git a/src/functional_test_runner/index.ts b/src/functional_test_runner/index.ts deleted file mode 100644 index 50b4c1c2a63f..000000000000 --- a/src/functional_test_runner/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 { FunctionalTestRunner } from './functional_test_runner'; -export { readConfigFile } from './lib'; diff --git a/src/legacy/core_plugins/apm_oss/index.js b/src/legacy/core_plugins/apm_oss/index.js index 6c0c6d0e5fe5..0c281ec939bb 100644 --- a/src/legacy/core_plugins/apm_oss/index.js +++ b/src/legacy/core_plugins/apm_oss/index.js @@ -38,7 +38,7 @@ export default function apmOss(kibana) { spanIndices: Joi.string().default('apm-*'), metricsIndices: Joi.string().default('apm-*'), onboardingIndices: Joi.string().default('apm-*'), - apmAgentConfigurationIndex: Joi.string().default('.apm-agent-configuration') + apmAgentConfigurationIndex: Joi.string().default('.apm-agent-configuration'), }).default(); }, @@ -49,7 +49,7 @@ export default function apmOss(kibana) { 'transactionIndices', 'spanIndices', 'metricsIndices', - 'onboardingIndices' + 'onboardingIndices', ].map(type => server.config().get(`apm_oss.${type}`)))); } }); diff --git a/src/legacy/core_plugins/console/api_server/es_6_0/mappings.js b/src/legacy/core_plugins/console/api_server/es_6_0/mappings.js index 187dfa2f12f3..28850dfd6de7 100644 --- a/src/legacy/core_plugins/console/api_server/es_6_0/mappings.js +++ b/src/legacy/core_plugins/console/api_server/es_6_0/mappings.js @@ -99,9 +99,14 @@ export default function (api) { }, boost: 1.0, null_value: '', - + doc_values: BOOLEAN, + eager_global_ordinals: BOOLEAN, norms: BOOLEAN, + // Not actually available in V6 of ES. Add when updating the autocompletion system. + // index_phrases: BOOLEAN, + // index_prefixes: { min_chars, max_chars }, + index_options: { __one_of: ['docs', 'freqs', 'positions'], }, diff --git a/src/legacy/core_plugins/console/public/_app.scss b/src/legacy/core_plugins/console/public/_app.scss index a694ea4f814b..f3de2a9ee9e5 100644 --- a/src/legacy/core_plugins/console/public/_app.scss +++ b/src/legacy/core_plugins/console/public/_app.scss @@ -61,3 +61,7 @@ z-index: $euiZLevel1 + 2; margin-top: 22px; } + +.conApp__settingsModal { + min-width: 460px; +} diff --git a/src/legacy/core_plugins/console/public/src/components/editor_example.tsx b/src/legacy/core_plugins/console/public/src/components/editor_example.tsx index c67b2a364457..99309d7b8549 100644 --- a/src/legacy/core_plugins/console/public/src/components/editor_example.tsx +++ b/src/legacy/core_plugins/console/public/src/components/editor_example.tsx @@ -19,7 +19,7 @@ import React, { useEffect } from 'react'; // @ts-ignore -import exampleText from 'raw-loader!./helpExample.txt'; +import exampleText from 'raw-loader!./help_example.txt'; import $ from 'jquery'; // @ts-ignore import SenseEditor from '../sense_editor/editor'; diff --git a/src/legacy/core_plugins/console/public/src/components/helpExample.txt b/src/legacy/core_plugins/console/public/src/components/help_example.txt similarity index 100% rename from src/legacy/core_plugins/console/public/src/components/helpExample.txt rename to src/legacy/core_plugins/console/public/src/components/help_example.txt diff --git a/src/legacy/core_plugins/console/public/src/components/settings_modal.tsx b/src/legacy/core_plugins/console/public/src/components/settings_modal.tsx index ac540f18bff0..f3ec577e43b7 100644 --- a/src/legacy/core_plugins/console/public/src/components/settings_modal.tsx +++ b/src/legacy/core_plugins/console/public/src/components/settings_modal.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useState } from 'react'; +import React, { Fragment, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -42,7 +42,7 @@ export type AutocompleteOptions = 'fields' | 'indices' | 'templates'; interface Props { onSaveSettings: (newSettings: DevToolsSettings) => Promise; onClose: () => void; - refreshAutocompleteSettings: () => void; + refreshAutocompleteSettings: (selectedSettings: any) => void; settings: DevToolsSettings; } @@ -106,9 +106,70 @@ export function DevToolsSettingsModal(props: Props) { }); } + // It only makes sense to show polling options if the user needs to fetch any data. + const pollingFields = + fields || indices || templates ? ( + + + } + helpText={ + + } + > + + } + onChange={e => setPolling(e.target.checked)} + /> + + + { + // Only refresh the currently selected settings. + props.refreshAutocompleteSettings({ + autocomplete: { + fields, + indices, + templates, + }, + }); + }} + > + + + + ) : ( + undefined + ); + return ( - + + setTripleQuotes(e.target.checked)} /> + - - } - helpText={ - - } - > - - } - onChange={e => setPolling(e.target.checked)} - /> - - - - + {pollingFields} diff --git a/src/legacy/core_plugins/console/public/src/helpers/settings_show_modal.tsx b/src/legacy/core_plugins/console/public/src/helpers/settings_show_modal.tsx index a88955c0ca8b..c4f36d836dfd 100644 --- a/src/legacy/core_plugins/console/public/src/helpers/settings_show_modal.tsx +++ b/src/legacy/core_plugins/console/public/src/helpers/settings_show_modal.tsx @@ -20,7 +20,7 @@ import { I18nContext } from 'ui/i18n'; import React from 'react'; import ReactDOM from 'react-dom'; -import { DevToolsSettingsModal } from '../components/settings_modal'; +import { DevToolsSettingsModal, AutocompleteOptions } from '../components/settings_modal'; import { DevToolsSettings } from '../components/dev_tools_settings'; // @ts-ignore @@ -32,8 +32,8 @@ export function showSettingsModal() { const container = document.getElementById('consoleSettingsModal'); const curSettings = getCurrentSettings(); - const refreshAutocompleteSettings = () => { - mappings.retrieveAutoCompleteInfo(); + const refreshAutocompleteSettings = (selectedSettings: any) => { + mappings.retrieveAutoCompleteInfo(selectedSettings); }; const closeModal = () => { @@ -53,13 +53,30 @@ export function showSettingsModal() { newSettings: DevToolsSettings, prevSettings: DevToolsSettings ) => { - // We'll only retrieve settings if polling is on. - const isPollingChanged = prevSettings.polling !== newSettings.polling; + // We'll only retrieve settings if polling is on. The expectation here is that if the user + // disables polling it's because they want manual control over the fetch request (possibly + // because it's a very expensive request given their cluster and bandwidth). In that case, + // they would be unhappy with any request that's sent automatically. if (newSettings.polling) { const autocompleteDiff = getAutocompleteDiff(newSettings, prevSettings); - if (autocompleteDiff.length > 0) { - mappings.retrieveAutoCompleteInfo(newSettings.autocomplete); + + const isSettingsChanged = autocompleteDiff.length > 0; + const isPollingChanged = prevSettings.polling !== newSettings.polling; + + if (isSettingsChanged) { + // If the user has changed one of the autocomplete settings, then we'll fetch just the + // ones which have changed. + const changedSettings: any = autocompleteDiff.reduce( + (changedSettingsAccum: any, setting: string): any => { + changedSettingsAccum[setting] = + newSettings.autocomplete[setting as AutocompleteOptions]; + return changedSettingsAccum; + }, + {} + ); + mappings.retrieveAutoCompleteInfo(changedSettings); } else if (isPollingChanged) { + // If the user has turned polling on, then we'll fetch all selected autocomplete settings. mappings.retrieveAutoCompleteInfo(); } } diff --git a/src/legacy/core_plugins/console/public/src/mappings.js b/src/legacy/core_plugins/console/public/src/mappings.js index 36e8d04291f6..51864333ef1f 100644 --- a/src/legacy/core_plugins/console/public/src/mappings.js +++ b/src/legacy/core_plugins/console/public/src/mappings.js @@ -170,14 +170,6 @@ function getFieldNamesFromFieldMapping(fieldName, fieldMapping) { const fieldType = fieldMapping.type; - if (fieldType === 'multi_field') { - nestedFields = $.map(fieldMapping.fields, function (fieldMapping, fieldName) { - return getFieldNamesFromFieldMapping(fieldName, fieldMapping); - }); - - return applyPathSettings(nestedFields); - } - const ret = { name: fieldName, type: fieldType }; if (fieldMapping.index_name) { @@ -287,6 +279,16 @@ function retrieveSettings(settingsKey, settingsToRetrieve) { } // Retrieve all selected settings by default. +// TODO: We should refactor this to be easier to consume. Ideally this function should retrieve +// whatever settings are specified, otherwise just use the saved settings. This requires changing +// the behavior to not *clear* whatever settings have been unselected, but it's hard to tell if +// this is possible without altering the autocomplete behavior. These are the scenarios we need to +// support: +// 1. Manual refresh. Specify what we want. Fetch specified, leave unspecified alone. +// 2. Changed selection and saved: Specify what we want. Fetch changed and selected, leave +// unchanged alone (both selected and unselected). +// 3. Poll: Use saved. Fetch selected. Ignore unselected. + function retrieveAutoCompleteInfo(settingsToRetrieve = settings.getAutocomplete()) { if (pollTimeoutId) { clearTimeout(pollTimeoutId); diff --git a/src/legacy/core_plugins/console/public/tests/src/mapping.test.js b/src/legacy/core_plugins/console/public/tests/src/mapping.test.js index c5ef5ed43bb3..d79f3c50b837 100644 --- a/src/legacy/core_plugins/console/public/tests/src/mapping.test.js +++ b/src/legacy/core_plugins/console/public/tests/src/mapping.test.js @@ -44,37 +44,6 @@ describe('Mappings', () => { return { name: name, type: type || 'string' }; } - test('Multi fields', function () { - mappings.loadMappings({ - index: { - properties: { - first_name: { - type: 'multi_field', - path: 'just_name', - fields: { - first_name: { type: 'string', index: 'analyzed' }, - any_name: { type: 'string', index: 'analyzed' }, - }, - }, - last_name: { - type: 'multi_field', - path: 'just_name', - fields: { - last_name: { type: 'string', index: 'analyzed' }, - any_name: { type: 'string', index: 'analyzed' }, - }, - }, - }, - }, - }); - - expect(mappings.getFields('index').sort(fc)).toEqual([ - f('any_name', 'string'), - f('first_name', 'string'), - f('last_name', 'string'), - ]); - }); - test('Multi fields 1.0 style', function () { mappings.loadMappings({ index: { diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts index 648204bf987e..b7f5b948697d 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { SavedObjectMetaData } from 'ui/saved_objects/components/saved_object_finder'; -import { SavedObjectAttributes } from 'target/types/server'; +import { SavedObjectAttributes } from 'src/core/server'; import { ContainerOutput, embeddableFactories, diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts index a3909bc556b5..1e71e2b26970 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_core.test.mocks.ts @@ -17,35 +17,11 @@ * under the License. */ -import { fatalErrorsServiceMock, notificationServiceMock } from '../../../../core/public/mocks'; - let modalContents: React.Component; export const getModalContents = () => modalContents; -jest.doMock('ui/new_platform', () => { - return { - npStart: { - core: { - overlays: { - openFlyout: jest.fn(), - openModal: (component: React.Component) => { - modalContents = component; - return { - close: jest.fn(), - }; - }, - }, - }, - }, - npSetup: { - core: { - fatalErrors: fatalErrorsServiceMock.createSetupContract(), - notifications: notificationServiceMock.createSetupContract(), - }, - }, - }; -}); +jest.mock('ui/new_platform'); jest.doMock('ui/metadata', () => ({ metadata: { diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts index bb2ca806bbac..043e1fc74c53 100644 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts @@ -41,6 +41,7 @@ export type getInitialContextFunction = () => InitialContextObject; export interface Handlers { getInitialContext: getInitialContextFunction; inspectorAdapters?: Adapters; + abortSignal?: AbortSignal; } type Context = object; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/directive.js b/src/legacy/core_plugins/data/public/filter/apply_filters/directive.js deleted file mode 100644 index f235bb3dab60..000000000000 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/directive.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. - */ - -import 'ngreact'; -import { uiModules } from 'ui/modules'; -import template from './directive.html'; -import { ApplyFiltersPopover } from './apply_filters_popover'; -import { mapAndFlattenFilters } from '../filter_manager/lib/map_and_flatten_filters'; -import { wrapInI18nContext } from 'ui/i18n'; - -const app = uiModules.get('app/data', ['react']); - -export function setupDirective() { - app.directive('applyFiltersPopoverComponent', (reactDirective) => { - return reactDirective(wrapInI18nContext(ApplyFiltersPopover)); - }); - - app.directive('applyFiltersPopover', (indexPatterns) => { - return { - template, - restrict: 'E', - scope: { - filters: '=', - onCancel: '=', - onSubmit: '=', - }, - link: function ($scope) { - $scope.state = {}; - - // Each time the new filters change we want to rebuild (not just re-render) the "apply filters" - // popover, because it has to reset its state whenever the new filters change. Setting a `key` - // property on the component accomplishes this due to how React handles the `key` property. - $scope.$watch('filters', filters => { - mapAndFlattenFilters(indexPatterns, filters).then(mappedFilters => { - $scope.state = { - filters: mappedFilters, - key: Date.now(), - }; - }); - }); - } - }; - }); -} diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts b/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts index 36d2501e1d9f..6b64230ed6a0 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts +++ b/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts @@ -18,6 +18,3 @@ */ export { ApplyFiltersPopover } from './apply_filters_popover'; - -// @ts-ignore -export { setupDirective } from './directive'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/directive.js b/src/legacy/core_plugins/data/public/filter/filter_bar/directive.js deleted file mode 100644 index 50559cca3ffa..000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/directive.js +++ /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 'ngreact'; -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from 'ui/modules'; -import { FilterBar } from './filter_bar'; - -const app = uiModules.get('app/kibana', ['react']); - -export function setupDirective() { - app.directive('filterBar', reactDirective => { - return reactDirective(wrapInI18nContext(FilterBar)); - }); -} diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx index ebb6c433450a..1bb594c183bc 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx @@ -32,6 +32,7 @@ import { EuiSwitch, } from '@elastic/eui'; import { FieldFilter, Filter } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import React, { Component } from 'react'; @@ -293,7 +294,11 @@ class FilterEditorUI extends Component { private renderCustomEditor() { return ( - + { if (isCustomEditorOpen) { const { index, disabled, negate } = this.props.filter.meta; - const newIndex = index || this.props.indexPatterns[0].id; + const newIndex = index || this.props.indexPatterns[0].id!; const body = JSON.parse(queryDsl); const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, $state.store); this.props.onSubmit(filter); } else if (indexPattern && field && operator) { - const filter = buildFilter(indexPattern, field, operator, params, alias, $state.store); + const filter = buildFilter( + indexPattern, + field, + operator, + this.props.filter.meta.disabled, + params, + alias, + $state.store + ); this.props.onSubmit(filter); } }; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts index 8190146a4258..384fb68b7966 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -18,8 +18,8 @@ */ import { FilterStateStore, toggleFilterNegated } from '@kbn/es-query'; - import { fixtures } from '../../../../index_patterns'; +import { IndexPattern, Field } from '../../../../index'; import { buildFilter, getFieldFromFilter, @@ -58,6 +58,8 @@ jest.mock( ); const { mockFields, mockIndexPattern } = fixtures; +const mockedFields = mockFields as Field[]; +const mockedIndexPattern = mockIndexPattern as IndexPattern; describe('Filter editor utils', () => { describe('getQueryDslFromFilter', () => { @@ -70,14 +72,14 @@ describe('Filter editor utils', () => { describe('getIndexPatternFromFilter', () => { it('should return the index pattern from the filter', () => { - const indexPattern = getIndexPatternFromFilter(phraseFilter, [mockIndexPattern]); - expect(indexPattern).toBe(mockIndexPattern); + const indexPattern = getIndexPatternFromFilter(phraseFilter, [mockedIndexPattern]); + expect(indexPattern).toBe(mockedIndexPattern); }); }); describe('getFieldFromFilter', () => { it('should return the field from the filter', () => { - const field = getFieldFromFilter(phraseFilter, mockIndexPattern); + const field = getFieldFromFilter(phraseFilter, mockedIndexPattern); expect(field).not.toBeUndefined(); expect(field && field.name).toBe(phraseFilter.meta.key); }); @@ -169,12 +171,12 @@ describe('Filter editor utils', () => { describe('getFilterableFields', () => { it('returns the list of fields from the given index pattern', () => { - const fieldOptions = getFilterableFields(mockIndexPattern); + const fieldOptions = getFilterableFields(mockedIndexPattern); expect(fieldOptions.length).toBeGreaterThan(0); }); it('limits the fields to the filterable fields', () => { - const fieldOptions = getFilterableFields(mockIndexPattern); + const fieldOptions = getFilterableFields(mockedIndexPattern); const nonFilterableFields = fieldOptions.filter(field => !field.filterable); expect(nonFilterableFields.length).toBe(0); }); @@ -183,14 +185,14 @@ describe('Filter editor utils', () => { describe('getOperatorOptions', () => { it('returns range for number fields', () => { const [field] = mockFields.filter(({ type }) => type === 'number'); - const operatorOptions = getOperatorOptions(field); + const operatorOptions = getOperatorOptions(field as Field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).not.toBeUndefined(); }); it('does not return range for string fields', () => { const [field] = mockFields.filter(({ type }) => type === 'string'); - const operatorOptions = getOperatorOptions(field); + const operatorOptions = getOperatorOptions(field as Field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).toBeUndefined(); }); @@ -198,44 +200,49 @@ describe('Filter editor utils', () => { describe('isFilterValid', () => { it('should return false if index pattern is not provided', () => { - const isValid = isFilterValid(undefined, mockFields[0], isOperator, 'foo'); + const isValid = isFilterValid(undefined, mockedFields[0], isOperator, 'foo'); expect(isValid).toBe(false); }); it('should return false if field is not provided', () => { - const isValid = isFilterValid(mockIndexPattern, undefined, isOperator, 'foo'); + const isValid = isFilterValid(mockedIndexPattern, undefined, isOperator, 'foo'); expect(isValid).toBe(false); }); it('should return false if operator is not provided', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], undefined, 'foo'); + const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], undefined, 'foo'); expect(isValid).toBe(false); }); it('should return false for phrases filter without phrases', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isOneOfOperator, []); + const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, []); expect(isValid).toBe(false); }); it('should return true for phrases filter with phrases', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isOneOfOperator, ['foo']); + const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, ['foo']); expect(isValid).toBe(true); }); it('should return false for range filter without range', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isBetweenOperator, undefined); + const isValid = isFilterValid( + mockedIndexPattern, + mockedFields[0], + isBetweenOperator, + undefined + ); expect(isValid).toBe(false); }); it('should return true for range filter with from', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isBetweenOperator, { + const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { from: 'foo', }); expect(isValid).toBe(true); }); it('should return true for range filter with from/to', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isBetweenOperator, { + const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { from: 'foo', too: 'goo', }); @@ -243,7 +250,7 @@ describe('Filter editor utils', () => { }); it('should return true for exists filter without params', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], existsOperator); + const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], existsOperator); expect(isValid).toBe(true); }); }); @@ -253,7 +260,15 @@ describe('Filter editor utils', () => { const params = 'foo'; const alias = 'bar'; const state = FilterStateStore.APP_STATE; - const filter = buildFilter(mockIndexPattern, mockFields[0], isOperator, params, alias, state); + const filter = buildFilter( + mockedIndexPattern, + mockedFields[0], + isOperator, + false, + params, + alias, + state + ); expect(filter.meta.negate).toBe(isOperator.negate); expect(filter.meta.alias).toBe(alias); @@ -268,9 +283,10 @@ describe('Filter editor utils', () => { const alias = 'bar'; const state = FilterStateStore.APP_STATE; const filter = buildFilter( - mockIndexPattern, - mockFields[0], + mockedIndexPattern, + mockedFields[0], isOneOfOperator, + false, params, alias, state @@ -289,9 +305,10 @@ describe('Filter editor utils', () => { const alias = 'bar'; const state = FilterStateStore.APP_STATE; const filter = buildFilter( - mockIndexPattern, - mockFields[0], + mockedIndexPattern, + mockedFields[0], isBetweenOperator, + false, params, alias, state @@ -309,9 +326,10 @@ describe('Filter editor utils', () => { const alias = 'bar'; const state = FilterStateStore.APP_STATE; const filter = buildFilter( - mockIndexPattern, - mockFields[0], + mockedIndexPattern, + mockedFields[0], existsOperator, + false, params, alias, state @@ -324,14 +342,31 @@ describe('Filter editor utils', () => { } }); + it('should include disabled state', () => { + const params = undefined; + const alias = 'bar'; + const state = FilterStateStore.APP_STATE; + const filter = buildFilter( + mockedIndexPattern, + mockedFields[0], + doesNotExistOperator, + true, + params, + alias, + state + ); + expect(filter.meta.disabled).toBe(true); + }); + it('should negate based on operator', () => { const params = undefined; const alias = 'bar'; const state = FilterStateStore.APP_STATE; const filter = buildFilter( - mockIndexPattern, - mockFields[0], + mockedIndexPattern, + mockedFields[0], doesNotExistOperator, + false, params, alias, state diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts index a4704e2b1d64..3b7c8c1feb44 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -130,6 +130,7 @@ export function buildFilter( indexPattern: IndexPattern, field: Field, operator: Operator, + disabled: boolean, params: any, alias: string | null, store: FilterStateStore @@ -137,6 +138,7 @@ export function buildFilter( const filter = buildBaseFilter(indexPattern, field, operator, params); filter.meta.alias = alias; filter.meta.negate = operator.negate; + filter.meta.disabled = disabled; filter.$state = { store }; return filter; } diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts index d0786734e42d..438d292b9f58 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts @@ -17,9 +17,4 @@ * under the License. */ -import './directive'; - export { FilterBar } from './filter_bar'; - -// @ts-ignore -export { setupDirective } from './directive'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts index d27b0eab0e34..3f3cbd0044a0 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts @@ -26,6 +26,7 @@ import { Filter, FilterStateStore } from '@kbn/es-query'; import { FilterStateManager } from './filter_state_manager'; import { FilterManager } from './filter_manager'; +import { IndexPatterns } from 'ui/index_patterns'; import { getFilter } from './test_helpers/get_stub_filter'; import { StubIndexPatterns } from './test_helpers/stub_index_pattern'; import { StubState } from './test_helpers/stub_state'; @@ -78,7 +79,7 @@ describe('filter_manager', () => { appStateStub = new StubState(); globalStateStub = new StubState(); indexPatterns = new StubIndexPatterns(); - filterManager = new FilterManager(indexPatterns); + filterManager = new FilterManager(indexPatterns as IndexPatterns); readyFilters = getFiltersArray(); // FilterStateManager is tested indirectly. @@ -217,6 +218,30 @@ describe('filter_manager', () => { expect(updateListener.called).toBeTruthy(); expect(updateListener.callCount).toBe(2); }); + + test('changing a disabled filter should fire only update event', async function() { + const updateStub = jest.fn(); + const fetchStub = jest.fn(); + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, true, false, 'age', 34); + + await filterManager.setFilters([f1]); + + filterManager.getUpdates$().subscribe({ + next: updateStub, + }); + + filterManager.getFetches$().subscribe({ + next: fetchStub, + }); + + const f2 = _.cloneDeep(f1); + f2.meta.negate = true; + await filterManager.setFilters([f2]); + + // this time, events should be emitted + expect(fetchStub).toBeCalledTimes(0); + expect(updateStub).toBeCalledTimes(1); + }); }); describe('add filters', () => { diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts index 6c472ab76f9c..ccb26c801c70 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts @@ -35,6 +35,8 @@ import { extractTimeFilter } from './lib/extract_time_filter'; // @ts-ignore import { changeTimeFilter } from './lib/change_time_filter'; +import { onlyDisabledFiltersChanged } from './lib/only_disabled'; + import { PartitionedFilters } from './partitioned_filters'; import { IndexPatterns } from '../../index_patterns'; @@ -92,13 +94,14 @@ export class FilterManager { }); const filtersUpdated = !_.isEqual(this.filters, newFilters); + const updatedOnlyDisabledFilters = onlyDisabledFiltersChanged(newFilters, this.filters); this.filters = newFilters; if (filtersUpdated) { this.updated$.next(); - // Fired together with updated$, because historically (~4 years ago) there was a fetch optimization, that didn't call fetch for very specific cases. - // This optimization seems irrelevant at the moment, but I didn't want to change the logic of all consumers. - this.fetch$.next(); + if (!updatedOnlyDisabledFilters) { + this.fetch$.next(); + } } } diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts index b413efc0ba0f..4152a0931b03 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts @@ -20,10 +20,9 @@ import sinon from 'sinon'; import { FilterStateStore } from '@kbn/es-query'; - -import { Subscription } from 'rxjs'; import { FilterStateManager } from './filter_state_manager'; +import { IndexPatterns } from 'ui/index_patterns'; import { StubState } from './test_helpers/stub_state'; import { getFilter } from './test_helpers/get_stub_filter'; import { FilterManager } from './filter_manager'; @@ -50,20 +49,13 @@ describe('filter_state_manager', () => { let appStateStub: StubState; let globalStateStub: StubState; - let subscription: Subscription | undefined; let filterManager: FilterManager; beforeEach(() => { appStateStub = new StubState(); globalStateStub = new StubState(); const indexPatterns = new StubIndexPatterns(); - filterManager = new FilterManager(indexPatterns); - }); - - afterEach(() => { - if (subscription) { - subscription.unsubscribe(); - } + filterManager = new FilterManager(indexPatterns as IndexPatterns); }); describe('app_state_undefined', () => { @@ -164,4 +156,25 @@ describe('filter_state_manager', () => { sinon.assert.calledOnce(globalStateStub.save); }); }); + + describe('bug fixes', () => { + /* + ** This test is here to reproduce a bug where a filter manager update + ** would cause filter state manager detects those changes + ** And triggers *another* filter manager update. + */ + test('should NOT re-trigger filter manager', async done => { + const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + filterManager.setFilters([f1]); + const setFiltersSpy = sinon.spy(filterManager, 'setFilters'); + + f1.meta.negate = true; + await filterManager.setFilters([f1]); + + setTimeout(() => { + expect(setFiltersSpy.callCount).toEqual(1); + done(); + }, 100); + }); + }); }); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts index 032233cbc0a8..06f91e35db96 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Filter, FilterStateStore } from '@kbn/es-query'; +import { FilterStateStore } from '@kbn/es-query'; import _ from 'lodash'; import { State } from 'ui/state_management/state'; @@ -34,8 +34,6 @@ export class FilterStateManager { filterManager: FilterManager; globalState: State; getAppState: GetAppStateFunc; - prevGlobalFilters: Filter[] | undefined; - prevAppFilters: Filter[] | undefined; interval: NodeJS.Timeout | undefined; constructor(globalState: State, getAppState: GetAppStateFunc, filterManager: FilterManager) { @@ -67,10 +65,8 @@ export class FilterStateManager { const globalFilters = this.globalState.filters || []; const appFilters = (appState && appState.filters) || []; - const globalFilterChanged = !( - this.prevGlobalFilters && _.isEqual(this.prevGlobalFilters, globalFilters) - ); - const appFilterChanged = !(this.prevAppFilters && _.isEqual(this.prevAppFilters, appFilters)); + const globalFilterChanged = !_.isEqual(this.filterManager.getGlobalFilters(), globalFilters); + const appFilterChanged = !_.isEqual(this.filterManager.getAppFilters(), appFilters); const filterStateChanged = globalFilterChanged || appFilterChanged; if (!filterStateChanged) return; @@ -81,10 +77,6 @@ export class FilterStateManager { FilterManager.setFiltersStore(newGlobalFilters, FilterStateStore.GLOBAL_STATE); this.filterManager.setFilters(newGlobalFilters.concat(newAppFilters)); - - // store new filter changes - this.prevGlobalFilters = newGlobalFilters; - this.prevAppFilters = newAppFilters; }, 10); } diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts index 0e37e1ed4dec..dc70ae910835 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts @@ -22,3 +22,4 @@ export { FilterStateManager } from './filter_state_manager'; // @ts-ignore export { uniqFilters } from './lib/uniq_filters'; +export { onlyDisabledFiltersChanged } from './lib/only_disabled'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_match_all.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_match_all.js index 8560784da98a..e0d8fdc88f44 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_match_all.js +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_match_all.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { mapMatchAll } from '../map_match_all'; -describe('ui/filter_manager/lib', function () { +describe('filter_manager/lib', function () { describe('mapMatchAll()', function () { let filter; diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/only_disabled.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/only_disabled.js deleted file mode 100644 index a6f4c74a70ba..000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/only_disabled.js +++ /dev/null @@ -1,130 +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 { onlyDisabled } from '../only_disabled'; -import expect from '@kbn/expect'; - -describe('Filter Bar Directive', function () { - describe('onlyDisabled()', function () { - - it('should return true if all filters are disabled', function () { - const filters = [ - { meta: { disabled: true } }, - { meta: { disabled: true } }, - { meta: { disabled: true } } - ]; - const newFilters = [{ meta: { disabled: true } }]; - expect(onlyDisabled(newFilters, filters)).to.be(true); - }); - - it('should return false if all filters are not disabled', function () { - const filters = [ - { meta: { disabled: false } }, - { meta: { disabled: false } }, - { meta: { disabled: false } } - ]; - const newFilters = [{ meta: { disabled: false } }]; - expect(onlyDisabled(newFilters, filters)).to.be(false); - }); - - it('should return false if only old filters are disabled', function () { - const filters = [ - { meta: { disabled: true } }, - { meta: { disabled: true } }, - { meta: { disabled: true } } - ]; - const newFilters = [{ meta: { disabled: false } }]; - expect(onlyDisabled(newFilters, filters)).to.be(false); - }); - - it('should return false if new filters are not disabled', function () { - const filters = [ - { meta: { disabled: false } }, - { meta: { disabled: false } }, - { meta: { disabled: false } } - ]; - const newFilters = [{ meta: { disabled: true } }]; - expect(onlyDisabled(newFilters, filters)).to.be(false); - }); - - it('should return true when all removed filters were disabled', function () { - const filters = [ - { meta: { disabled: true } }, - { meta: { disabled: true } }, - { meta: { disabled: true } } - ]; - const newFilters = []; - expect(onlyDisabled(newFilters, filters)).to.be(true); - }); - - it('should return false when all removed filters were not disabled', function () { - const filters = [ - { meta: { disabled: false } }, - { meta: { disabled: false } }, - { meta: { disabled: false } } - ]; - const newFilters = []; - expect(onlyDisabled(newFilters, filters)).to.be(false); - }); - - it('should return true if all changed filters are disabled', function () { - const filters = [ - { meta: { disabled: true, negate: false } }, - { meta: { disabled: true, negate: false } } - ]; - const newFilters = [ - { meta: { disabled: true, negate: true } }, - { meta: { disabled: true, negate: true } } - ]; - expect(onlyDisabled(newFilters, filters)).to.be(true); - }); - - it('should return false if all filters remove were not disabled', function () { - const filters = [ - { meta: { disabled: false } }, - { meta: { disabled: false } }, - { meta: { disabled: true } } - ]; - const newFilters = [{ meta: { disabled: false } }]; - expect(onlyDisabled(newFilters, filters)).to.be(false); - }); - - it('should return false when all removed filters are not disabled', function () { - const filters = [ - { meta: { disabled: true } }, - { meta: { disabled: false } }, - { meta: { disabled: true } } - ]; - const newFilters = []; - expect(onlyDisabled(newFilters, filters)).to.be(false); - }); - - it('should not throw with null filters', function () { - const filters = [ - null, - { meta: { disabled: true } } - ]; - const newFilters = []; - expect(function () { - onlyDisabled(newFilters, filters); - }).to.not.throwError(); - }); - - }); -}); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.js deleted file mode 100644 index b78fde4e2820..000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.js +++ /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. - */ - -import _ from 'lodash'; - -const pluckDisabled = function (filter) { - return _.get(filter, 'meta.disabled'); -}; - -/** - * Checks to see if only disabled filters have been changed - * @returns {bool} Only disabled filters - */ -export function onlyDisabled(newFilters, oldFilters) { - return _.every(newFilters.concat(oldFilters), function (newFilter) { - return pluckDisabled(newFilter); - }); -} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.test.ts new file mode 100644 index 000000000000..7a3b767b97b1 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.test.ts @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Filter } from '@kbn/es-query'; + +import { onlyDisabledFiltersChanged } from './only_disabled'; +import expect from '@kbn/expect'; + +describe('Filter Bar Directive', function() { + describe('onlyDisabledFiltersChanged()', function() { + it('should return true if all filters are disabled', function() { + const filters = [ + { meta: { disabled: true } }, + { meta: { disabled: true } }, + { meta: { disabled: true } }, + ] as Filter[]; + const newFilters = [{ meta: { disabled: true } }] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, filters)).to.be(true); + }); + + it('should return false if there are no old filters', function() { + const newFilters = [{ meta: { disabled: false } }] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, undefined)).to.be(false); + }); + + it('should return false if there are no new filters', function() { + const filters = [{ meta: { disabled: false } }] as Filter[]; + expect(onlyDisabledFiltersChanged(undefined, filters)).to.be(false); + }); + + it('should return false if all filters are not disabled', function() { + const filters = [ + { meta: { disabled: false } }, + { meta: { disabled: false } }, + { meta: { disabled: false } }, + ] as Filter[]; + const newFilters = [{ meta: { disabled: false } }] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, filters)).to.be(false); + }); + + it('should return false if only old filters are disabled', function() { + const filters = [ + { meta: { disabled: true } }, + { meta: { disabled: true } }, + { meta: { disabled: true } }, + ] as Filter[]; + const newFilters = [{ meta: { disabled: false } }] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, filters)).to.be(false); + }); + + it('should return false if new filters are not disabled', function() { + const filters = [ + { meta: { disabled: false } }, + { meta: { disabled: false } }, + { meta: { disabled: false } }, + ] as Filter[]; + const newFilters = [{ meta: { disabled: true } }] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, filters)).to.be(false); + }); + + it('should return true when all removed filters were disabled', function() { + const filters = [ + { meta: { disabled: true } }, + { meta: { disabled: true } }, + { meta: { disabled: true } }, + ] as Filter[]; + const newFilters = [] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, filters)).to.be(true); + }); + + it('should return false when all removed filters were not disabled', function() { + const filters = [ + { meta: { disabled: false } }, + { meta: { disabled: false } }, + { meta: { disabled: false } }, + ] as Filter[]; + const newFilters = [] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, filters)).to.be(false); + }); + + it('should return true if all changed filters are disabled', function() { + const filters = [ + { meta: { disabled: true, negate: false } }, + { meta: { disabled: true, negate: false } }, + ] as Filter[]; + const newFilters = [ + { meta: { disabled: true, negate: true } }, + { meta: { disabled: true, negate: true } }, + ] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, filters)).to.be(true); + }); + + it('should return false if all filters remove were not disabled', function() { + const filters = [ + { meta: { disabled: false } }, + { meta: { disabled: false } }, + { meta: { disabled: true } }, + ] as Filter[]; + const newFilters = [{ meta: { disabled: false } }] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, filters)).to.be(false); + }); + + it('should return false when all removed filters are not disabled', function() { + const filters = [ + { meta: { disabled: true } }, + { meta: { disabled: false } }, + { meta: { disabled: true } }, + ] as Filter[]; + const newFilters = [] as Filter[]; + expect(onlyDisabledFiltersChanged(newFilters, filters)).to.be(false); + }); + + it('should not throw with null filters', function() { + const filters = [null, { meta: { disabled: true } }] as Filter[]; + const newFilters = [] as Filter[]; + expect(function() { + onlyDisabledFiltersChanged(newFilters, filters); + }).to.not.throwError(); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.ts new file mode 100644 index 000000000000..24f6b6db5352 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.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 _ from 'lodash'; +import { Filter } from '@kbn/es-query'; + +const isEnabled = function(filter: Filter) { + return filter && filter.meta && !filter.meta.disabled; +}; +/** + * Checks to see if only disabled filters have been changed + * @returns {bool} Only disabled filters + */ +export function onlyDisabledFiltersChanged(newFilters?: Filter[], oldFilters?: Filter[]) { + // If it's the same - compare only enabled filters + const newEnabledFilters = _.filter(newFilters || [], isEnabled); + const oldEnabledFilters = _.filter(oldFilters || [], isEnabled); + + return _.isEqual(oldEnabledFilters, newEnabledFilters); +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_state_changed.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_state_changed.js deleted file mode 100644 index 73e35bfec07e..000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_state_changed.js +++ /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 _ from 'lodash'; -import { compareFilters } from './compare_filters'; -const compareOptions = { disabled: true, negate: true }; - -/** - * Checks to see if only disabled filters have been changed - * @returns {bool} Only disabled filters - */ -export function onlyStateChanged(newFilters, oldFilters) { - return _.every(newFilters, function (newFilter) { - const match = _.find(oldFilters, function (oldFilter) { - return compareFilters(newFilter, oldFilter, compareOptions); - }); - return !!match; - }); -} diff --git a/src/legacy/core_plugins/data/public/filter/filter_service.ts b/src/legacy/core_plugins/data/public/filter/filter_service.ts index 2dbe8da3e2ad..27cf027e4590 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_service.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_service.ts @@ -17,9 +17,6 @@ * under the License. */ -import { once } from 'lodash'; -import { FilterBar, setupDirective as setupFilterBarDirective } from './filter_bar'; -import { ApplyFiltersPopover, setupDirective as setupApplyFiltersDirective } from './apply_filters'; import { IndexPatterns } from '../index_patterns'; import { FilterManager } from './filter_manager'; /** @@ -37,14 +34,6 @@ export class FilterService { return { filterManager, - ui: { - ApplyFiltersPopover, - FilterBar, - }, - loadLegacyDirectives: once(() => { - setupFilterBarDirective(); - setupApplyFiltersDirective(); - }), }; } diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/legacy/core_plugins/data/public/filter/index.tsx index d4dba0d834ff..24452d3c3557 100644 --- a/src/legacy/core_plugins/data/public/filter/index.tsx +++ b/src/legacy/core_plugins/data/public/filter/index.tsx @@ -20,3 +20,5 @@ export { FilterService, FilterSetup } from './filter_service'; export { FilterBar } from './filter_bar'; + +export { ApplyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 9cce64b0c574..4f9f3b323fbb 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -17,75 +17,30 @@ * under the License. */ -// TODO these are imports from the old plugin world. -// Once the new platform is ready, they can get removed -// and handled by the platform itself in the setup method -// of the ExpressionExectorService -// @ts-ignore -import { renderersRegistry } from 'plugins/interpreter/registries'; -import { ExpressionsService, ExpressionsSetup } from './expressions'; -import { QueryService, QuerySetup } from './query'; -import { FilterService, FilterSetup } from './filter'; -import { IndexPatternsService, IndexPatternsSetup } from './index_patterns'; +// /// Define plugin function +import { DataPlugin as Plugin, DataSetup } from './plugin'; -export class DataPlugin { - // Exposed services, sorted alphabetically - private readonly expressions: ExpressionsService; - private readonly filter: FilterService; - private readonly indexPatterns: IndexPatternsService; - private readonly query: QueryService; - - constructor() { - this.indexPatterns = new IndexPatternsService(); - this.filter = new FilterService(); - this.query = new QueryService(); - this.expressions = new ExpressionsService(); - } - - public setup(): DataSetup { - // TODO: this is imported here to avoid circular imports. - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { getInterpreter } = require('plugins/interpreter/interpreter'); - const indexPatternsService = this.indexPatterns.setup(); - return { - expressions: this.expressions.setup({ - interpreter: { - getInterpreter, - renderersRegistry, - }, - }), - indexPatterns: indexPatternsService, - filter: this.filter.setup({ - indexPatterns: indexPatternsService.indexPatterns, - }), - query: this.query.setup(), - }; - } - - public stop() { - this.expressions.stop(); - this.indexPatterns.stop(); - this.filter.stop(); - this.query.stop(); - } +export function plugin() { + return new Plugin(); } -/** @public */ -export interface DataSetup { - expressions: ExpressionsSetup; - indexPatterns: IndexPatternsSetup; - filter: FilterSetup; - query: QuerySetup; -} +// /// Export types & static code /** @public types */ +export type DataSetup = DataSetup; export { ExpressionRenderer, ExpressionRendererProps, ExpressionRunner } from './expressions'; /** @public types */ -export { IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field } from './index_patterns'; -export { Query, QueryBar } from './query'; -export { FilterBar } from './filter'; -export { FilterManager, FilterStateManager, uniqFilters } from './filter/filter_manager'; +export { IndexPattern, IndexPatterns, StaticIndexPattern, Field } from './index_patterns'; +export { Query, QueryBar, QueryBarInput } from './query'; +export { FilterBar, ApplyFiltersPopover } from './filter'; +export { SearchBar, SearchBarProps } from './search'; +export { + FilterManager, + FilterStateManager, + uniqFilters, + onlyDisabledFiltersChanged, +} from './filter/filter_manager'; /** @public static code */ export { dateHistogramInterval } from '../common/date_histogram_interval'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index.ts b/src/legacy/core_plugins/data/public/index_patterns/index.ts index 471c646ff767..ba211850d52f 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index.ts @@ -20,12 +20,12 @@ export { IndexPatternsService, IndexPatterns, + fixtures, + utils, // types IndexPatternsSetup, IndexPattern, StaticIndexPattern, - StaticIndexPatternField, Field, - fixtures, - utils, + FieldType, } from './index_patterns_service'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index 518a4b8c6c14..74918bebccf0 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -33,7 +33,7 @@ import { validateIndexPattern } from 'ui/index_patterns/index'; import { isFilterable, getFromSavedObject } from 'ui/index_patterns/static_utils'; -// IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field +// IndexPattern, StaticIndexPattern, Field import * as types from 'ui/index_patterns'; const config = chrome.getUiSettingsClient(); @@ -94,7 +94,7 @@ export type IndexPattern = types.IndexPattern; export type StaticIndexPattern = types.StaticIndexPattern; /** @public */ -export type StaticIndexPatternField = types.StaticIndexPatternField; +export type Field = types.Field; /** @public */ -export type Field = types.Field; +export type FieldType = types.FieldType; diff --git a/src/legacy/core_plugins/data/public/legacy.ts b/src/legacy/core_plugins/data/public/legacy.ts new file mode 100644 index 000000000000..14350a628419 --- /dev/null +++ b/src/legacy/core_plugins/data/public/legacy.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. + */ + +/** + * New Platform Shim + * + * In this file, we import any legacy dependencies we have, and shim them into + * our plugin by manually constructing the values that the new platform will + * eventually be passing to the `setup` method of our plugin definition. + * + * The idea is that our `plugin.ts` can stay "pure" and not contain any legacy + * world code. Then when it comes time to migrate to the new platform, we can + * simply delete this shim file. + * + * We are also calling `setup` here and exporting our public contract so that + * other legacy plugins are able to import from '../core_plugins/data/legacy' + * and receive the response value of the `setup` contract, mimicking the + * data that will eventually be injected by the new platform. + */ + +import { npSetup } from 'ui/new_platform'; +// @ts-ignore +import { renderersRegistry } from 'plugins/interpreter/registries'; +// @ts-ignore +import { getInterpreter } from 'plugins/interpreter/interpreter'; +import { LegacyDependenciesPlugin } from './shim/legacy_dependencies_plugin'; +import { plugin } from '.'; + +const dataPlugin = plugin(); +const legacyPlugin = new LegacyDependenciesPlugin(); + +export const setup = dataPlugin.setup(npSetup.core, { + __LEGACY: legacyPlugin.setup(), + interpreter: { + renderersRegistry, + getInterpreter, + }, +}); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts new file mode 100644 index 000000000000..f94e10e033a9 --- /dev/null +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, CoreStart, Plugin } from '../../../../core/public'; +import { ExpressionsService, ExpressionsSetup } from './expressions'; +import { SearchService, SearchSetup } from './search'; +import { QueryService, QuerySetup } from './query'; +import { FilterService, FilterSetup } from './filter'; +import { IndexPatternsService, IndexPatternsSetup } from './index_patterns'; +import { LegacyDependenciesPluginSetup } from './shim/legacy_dependencies_plugin'; + +/** + * Interface for any dependencies on other plugins' `setup` contracts. + * + * @internal + */ +export interface DataPluginSetupDependencies { + __LEGACY: LegacyDependenciesPluginSetup; + interpreter: any; +} + +/** + * Interface for this plugin's returned `setup` contract. + * + * @public + */ +export interface DataSetup { + expressions: ExpressionsSetup; + indexPatterns: IndexPatternsSetup; + filter: FilterSetup; + query: QuerySetup; + search: SearchSetup; +} + +/** + * Data Plugin - public + * + * This is the entry point for the entire client-side public contract of the plugin. + * If something is not explicitly exported here, you can safely assume it is private + * to the plugin and not considered stable. + * + * All stateful contracts will be injected by the platform at runtime, and are defined + * in the setup/start interfaces. The remaining items exported here are either types, + * or static code. + */ +export class DataPlugin implements Plugin { + // Exposed services, sorted alphabetically + private readonly expressions: ExpressionsService = new ExpressionsService(); + private readonly filter: FilterService = new FilterService(); + private readonly indexPatterns: IndexPatternsService = new IndexPatternsService(); + private readonly query: QueryService = new QueryService(); + private readonly search: SearchService = new SearchService(); + + public setup(core: CoreSetup, { __LEGACY, interpreter }: DataPluginSetupDependencies): DataSetup { + const indexPatternsService = this.indexPatterns.setup(); + return { + expressions: this.expressions.setup({ + interpreter, + }), + indexPatterns: indexPatternsService, + filter: this.filter.setup({ + indexPatterns: indexPatternsService.indexPatterns, + }), + query: this.query.setup(), + search: this.search.setup(), + }; + } + + public start(core: CoreStart) {} + + public stop() { + this.expressions.stop(); + this.indexPatterns.stop(); + this.filter.stop(); + this.query.stop(); + this.search.stop(); + } +} diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar.test.tsx.snap b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar.test.tsx.snap index 8680f269d93e..47838306620e 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar.test.tsx.snap +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar.test.tsx.snap @@ -79,6 +79,7 @@ exports[`QueryBar Should render the given query 1`] = ` dateFormat="YY" end="now" isAutoRefreshOnly={false} + isDisabled={false} isPaused={true} onTimeChange={[Function]} recentlyUsedRanges={ diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.tsx index 87bcebc4c510..6b56ad49a23c 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar.test.tsx @@ -23,6 +23,7 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import './query_bar.test.mocks'; import { QueryBar } from './query_bar'; +import { IndexPattern } from '../../../index'; const noop = () => { return; @@ -63,7 +64,7 @@ const mockIndexPattern = { searchable: true, }, ], -}; +} as IndexPattern; describe('QueryBar', () => { const QUERY_INPUT_SELECTOR = 'InjectIntl(QueryBarInputUI)'; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx index d51dda1e4f5d..ff155dfccdac 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx @@ -28,6 +28,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { QueryLanguageSwitcher } from './language_switcher'; import { QueryBarInput, QueryBarInputUI } from './query_bar_input'; +import { IndexPattern } from '../../../index'; const noop = () => { return; @@ -73,7 +74,7 @@ const mockIndexPattern = { searchable: true, }, ], -}; +} as IndexPattern; describe('QueryBarInput', () => { beforeEach(() => { diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx index 28a31610a40f..9bfdc6876372 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx @@ -111,7 +111,7 @@ export class QueryBarInputUI extends Component { indexPattern => typeof indexPattern !== 'string' ) as IndexPattern[]; - const objectPatternsFromStrings = await fetchIndexPatterns(stringPatterns); + const objectPatternsFromStrings = (await fetchIndexPatterns(stringPatterns)) as IndexPattern[]; this.setState({ indexPatterns: [...objectPatterns, ...objectPatternsFromStrings], diff --git a/src/legacy/core_plugins/data/public/query/query_service.ts b/src/legacy/core_plugins/data/public/query/query_service.ts index 745fb1bac686..db04b3a29a21 100644 --- a/src/legacy/core_plugins/data/public/query/query_service.ts +++ b/src/legacy/core_plugins/data/public/query/query_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { QueryBar, QueryBarInput, fromUser, toUser, getQueryLog } from './query_bar'; +import { fromUser, toUser, getQueryLog } from './query_bar'; /** * Query Service @@ -32,10 +32,6 @@ export class QueryService { toUser, getQueryLog, }, - ui: { - QueryBar, - QueryBarInput, - }, }; } @@ -47,4 +43,4 @@ export class QueryService { /** @public */ export type QuerySetup = ReturnType; -export { Query, QueryBar } from './query_bar'; +export { Query, QueryBar, QueryBarInput } from './query_bar'; diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts new file mode 100644 index 000000000000..6a9687ba7e32 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { SearchService, SearchSetup } from './search_service'; + +export * from './search_bar'; diff --git a/src/legacy/core_plugins/kibana_react/public/search_bar/components/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx similarity index 100% rename from src/legacy/core_plugins/kibana_react/public/search_bar/components/index.tsx rename to src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx diff --git a/src/legacy/core_plugins/kibana_react/public/search_bar/components/search_bar.test.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx similarity index 97% rename from src/legacy/core_plugins/kibana_react/public/search_bar/components/search_bar.test.tsx rename to src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx index 2a384aa54085..0fccf8bd6f5a 100644 --- a/src/legacy/core_plugins/kibana_react/public/search_bar/components/search_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx @@ -20,8 +20,9 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { SearchBar } from './search_bar'; +import { IndexPattern } from 'ui/index_patterns'; -jest.mock('../../../../data/public', () => { +jest.mock('../../../../../data/public', () => { return { FilterBar: () =>

    , QueryBar: () =>
    , @@ -60,7 +61,7 @@ const mockIndexPattern = { searchable: true, }, ], -}; +} as IndexPattern; const kqlQuery = { query: 'response:200', diff --git a/src/legacy/core_plugins/kibana_react/public/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx similarity index 90% rename from src/legacy/core_plugins/kibana_react/public/search_bar/components/search_bar.tsx rename to src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index cf6ebc947c1e..4edd82d2ebe3 100644 --- a/src/legacy/core_plugins/kibana_react/public/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -21,12 +21,13 @@ import { EuiFilterButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Filter } from '@kbn/es-query'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { Storage } from 'ui/storage'; -import { IndexPattern, Query, QueryBar, FilterBar } from '../../../../data/public'; +import { IndexPattern, Query, QueryBar, FilterBar } from '../../../../../data/public'; interface DateRange { from: string; @@ -107,21 +108,27 @@ class SearchBarUI extends Component { } private getFilterTriggerButton() { - const filtersAppliedText = this.props.intl.formatMessage({ - id: 'kibana_react.search.searchBar.filtersButtonFiltersAppliedTitle', - defaultMessage: 'filters applied.', - }); + const filterCount = this.getFilterLength(); + const filtersAppliedText = this.props.intl.formatMessage( + { + id: 'data.search.searchBar.searchBar.filtersButtonFiltersAppliedTitle', + defaultMessage: + '{filterCount} {filterCount, plural, one {filter} other {filters}} applied.', + }, + { + filterCount, + } + ); const clickToShowOrHideText = this.state.isFiltersVisible ? this.props.intl.formatMessage({ - id: 'kibana_react.search.searchBar.filtersButtonClickToShowTitle', + id: 'data.search.searchBar.searchBar.filtersButtonClickToShowTitle', defaultMessage: 'Select to hide', }) : this.props.intl.formatMessage({ - id: 'kibana_react.search.searchBar.filtersButtonClickToHideTitle', + id: 'data.search.searchBar.searchBar.filtersButtonClickToHideTitle', defaultMessage: 'Select to show', }); - const filterCount = this.getFilterLength(); return ( { aria-expanded={!!this.state.isFiltersVisible} title={`${filterCount ? filtersAppliedText : ''} ${clickToShowOrHideText}`} > - Filters + {i18n.translate('data.search.searchBar.searchBar.filtersButtonLabel', { + defaultMessage: 'Filters', + description: 'The noun "filter" in plural.', + })} ); } diff --git a/src/legacy/core_plugins/kibana_react/public/search_bar/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/index.tsx similarity index 100% rename from src/legacy/core_plugins/kibana_react/public/search_bar/index.tsx rename to src/legacy/core_plugins/data/public/search/search_bar/index.tsx diff --git a/src/legacy/core_plugins/data/public/search/search_service.ts b/src/legacy/core_plugins/data/public/search/search_service.ts new file mode 100644 index 000000000000..efebe89180ce --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/search_service.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Search Service + * @internal + */ + +export class SearchService { + public setup() { + return {}; + } + + public stop() {} +} + +/** @public */ + +export type SearchSetup = ReturnType; diff --git a/src/legacy/core_plugins/data/public/setup.ts b/src/legacy/core_plugins/data/public/setup.ts index 6329d2931ecd..a99a2a4d06ef 100644 --- a/src/legacy/core_plugins/data/public/setup.ts +++ b/src/legacy/core_plugins/data/public/setup.ts @@ -17,11 +17,7 @@ * under the License. */ -import { DataPlugin } from './index'; +import { setup } from './legacy'; -/** - * We export data here so that users importing from 'plugins/data' - * will automatically receive the response value of the `setup` contract, mimicking - * the data that will eventually be injected by the new platform. - */ -export const data = new DataPlugin().setup(); +// for backwards compatibility with 7.3 +export const data = setup; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/directive.html b/src/legacy/core_plugins/data/public/shim/apply_filter_directive.html similarity index 100% rename from src/legacy/core_plugins/data/public/filter/apply_filters/directive.html rename to src/legacy/core_plugins/data/public/shim/apply_filter_directive.html diff --git a/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts new file mode 100644 index 000000000000..4289d56b33c6 --- /dev/null +++ b/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.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 chrome from 'ui/chrome'; +import { CoreStart, Plugin } from '../../../../../../src/core/public'; +import { initLegacyModule } from './legacy_module'; + +/** @internal */ +export interface LegacyDependenciesPluginSetup { + savedObjectsClient: any; +} + +export class LegacyDependenciesPlugin implements Plugin { + public setup() { + initLegacyModule(); + + return { + savedObjectsClient: chrome.getSavedObjectsClient(), + } as LegacyDependenciesPluginSetup; + } + + public start(core: CoreStart) { + // nothing to do here yet + } +} diff --git a/src/legacy/core_plugins/data/public/shim/legacy_module.ts b/src/legacy/core_plugins/data/public/shim/legacy_module.ts new file mode 100644 index 000000000000..46849196e970 --- /dev/null +++ b/src/legacy/core_plugins/data/public/shim/legacy_module.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 { once } from 'lodash'; + +import { wrapInI18nContext } from 'ui/i18n'; +import { Filter } from '@kbn/es-query'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { IndexPatterns } from 'src/legacy/core_plugins/data/public'; +import { FilterBar, ApplyFiltersPopover } from '../filter'; +import template from './apply_filter_directive.html'; + +// @ts-ignore +import { mapAndFlattenFilters } from '../filter/filter_manager/lib/map_and_flatten_filters'; +// @ts-ignore + +/** @internal */ +export const initLegacyModule = once((): void => { + uiModules + .get('app/kibana') + .directive('filterBar', (reactDirective: any) => { + return reactDirective(wrapInI18nContext(FilterBar)); + }) + .directive('applyFiltersPopoverComponent', (reactDirective: any) => { + return reactDirective(wrapInI18nContext(ApplyFiltersPopover)); + }) + .directive('applyFiltersPopover', (indexPatterns: IndexPatterns) => { + return { + template, + restrict: 'E', + scope: { + filters: '=', + onCancel: '=', + onSubmit: '=', + }, + link($scope: any) { + $scope.state = {}; + + // Each time the new filters change we want to rebuild (not just re-render) the "apply filters" + // popover, because it has to reset its state whenever the new filters change. Setting a `key` + // property on the component accomplishes this due to how React handles the `key` property. + $scope.$watch('filters', (filters: any) => { + mapAndFlattenFilters(indexPatterns, filters).then((mappedFilters: Filter[]) => { + $scope.state = { + filters: mappedFilters, + key: Date.now(), + }; + }); + }); + }, + }; + }); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 42fca6c721b0..557ed941bbfa 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -38,8 +38,7 @@ import { EuiText, } from '@elastic/eui'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectAttributes } from 'src/core/server/saved_objects'; +import { SavedObjectAttributes } from 'src/core/server'; import { EmbeddableFactoryNotFoundError } from '../../../../embeddables/embeddable_factory_not_found_error'; import { IContainer } from '../../../../containers'; diff --git a/src/legacy/core_plugins/embeddable_api/public/ui_capabilities.test.mocks.ts b/src/legacy/core_plugins/embeddable_api/public/ui_capabilities.test.mocks.ts index b7e4a12fac24..69163522c2d9 100644 --- a/src/legacy/core_plugins/embeddable_api/public/ui_capabilities.test.mocks.ts +++ b/src/legacy/core_plugins/embeddable_api/public/ui_capabilities.test.mocks.ts @@ -24,3 +24,5 @@ jest.doMock('ui/capabilities', () => ({ }, }, })); + +jest.mock('ui/new_platform'); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/options_tab.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/options_tab.test.js.snap index bc333351b307..f78607e9cfa0 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/options_tab.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/options_tab.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders OptionsTab 1`] = ` +exports[`OptionsTab should renders OptionsTab 1`] = ` { - return await this.props.scope.vis.API.indexPatterns.get(indexPatternId); + return await this.props.vis.API.indexPatterns.get(indexPatternId); } - setVisParam(paramName, paramValue) { - const params = _.cloneDeep(this.props.editorState.params); - params[paramName] = paramValue; - this.props.stageEditorParams(params); - } + onChange = value => this.props.setValue('controls', value) handleLabelChange = (controlIndex, evt) => { - const updatedControl = this.props.editorState.params.controls[controlIndex]; + const updatedControl = this.props.stateParams.controls[controlIndex]; updatedControl.label = evt.target.value; - this.setVisParam('controls', setControl(this.props.editorState.params.controls, controlIndex, updatedControl)); + this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); } handleIndexPatternChange = (controlIndex, indexPatternId) => { - const updatedControl = this.props.editorState.params.controls[controlIndex]; + const updatedControl = this.props.stateParams.controls[controlIndex]; updatedControl.indexPattern = indexPatternId; updatedControl.fieldName = ''; - this.setVisParam('controls', setControl(this.props.editorState.params.controls, controlIndex, updatedControl)); + this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); } handleFieldNameChange = (controlIndex, fieldName) => { - const updatedControl = this.props.editorState.params.controls[controlIndex]; + const updatedControl = this.props.stateParams.controls[controlIndex]; updatedControl.fieldName = fieldName; - this.setVisParam('controls', setControl(this.props.editorState.params.controls, controlIndex, updatedControl)); + this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); } handleCheckboxOptionChange = (controlIndex, optionName, evt) => { - const updatedControl = this.props.editorState.params.controls[controlIndex]; + const updatedControl = this.props.stateParams.controls[controlIndex]; updatedControl.options[optionName] = evt.target.checked; - this.setVisParam('controls', setControl(this.props.editorState.params.controls, controlIndex, updatedControl)); + this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); } handleNumberOptionChange = (controlIndex, optionName, evt) => { - const updatedControl = this.props.editorState.params.controls[controlIndex]; + const updatedControl = this.props.stateParams.controls[controlIndex]; updatedControl.options[optionName] = parseFloat(evt.target.value); - this.setVisParam('controls', setControl(this.props.editorState.params.controls, controlIndex, updatedControl)); + this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); } handleRemoveControl = (controlIndex) => { - this.setVisParam('controls', removeControl(this.props.editorState.params.controls, controlIndex)); + this.onChange(removeControl(this.props.stateParams.controls, controlIndex)); } moveControl = (controlIndex, direction) => { - this.setVisParam('controls', moveControl(this.props.editorState.params.controls, controlIndex, direction)); + this.onChange(moveControl(this.props.stateParams.controls, controlIndex, direction)); } handleAddControl = () => { - this.setVisParam('controls', addControl(this.props.editorState.params.controls, newControl(this.state.type))); + this.onChange(addControl(this.props.stateParams.controls, newControl(this.state.type))); } handleParentChange = (controlIndex, evt) => { - const updatedControl = this.props.editorState.params.controls[controlIndex]; + const updatedControl = this.props.stateParams.controls[controlIndex]; updatedControl.parent = evt.target.value; - this.setVisParam('controls', setControl(this.props.editorState.params.controls, controlIndex, updatedControl)); + this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); } renderControls() { - const lineageMap = getLineageMap(this.props.editorState.params.controls); - return this.props.editorState.params.controls.map((controlParams, controlIndex) => { + const lineageMap = getLineageMap(this.props.stateParams.controls); + return this.props.stateParams.controls.map((controlParams, controlIndex) => { const parentCandidates = getParentCandidates( - this.props.editorState.params.controls, + this.props.stateParams.controls, controlParams.id, lineageMap); return ( @@ -187,8 +182,8 @@ class ControlsTabUi extends Component { } ControlsTabUi.propTypes = { - scope: PropTypes.object.isRequired, - stageEditorParams: PropTypes.func.isRequired + vis: PropTypes.object.isRequired, + setValue: PropTypes.func.isRequired }; export const ControlsTab = injectI18n(ControlsTabUi); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js index add17262860a..6e193b60e46e 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js @@ -17,8 +17,9 @@ * under the License. */ +jest.mock('ui/new_platform'); + import React from 'react'; -import sinon from 'sinon'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; @@ -29,15 +30,17 @@ import { const indexPatternsMock = { get: getIndexPatternMock }; -const scopeMock = { - vis: { - API: { - indexPatterns: indexPatternsMock +let props; + +beforeEach(() => { + props = { + vis: { + API: { + indexPatterns: indexPatternsMock + }, }, - }, - editorState: { - params: { - 'controls': [ + stateParams: { + controls: [ { 'id': '1', 'indexPattern': 'indexPattern1', @@ -62,138 +65,111 @@ const scopeMock = { } } ] - } - } -}; -let stageEditorParams; - -beforeEach(() => { - stageEditorParams = sinon.spy(); + }, + setValue: jest.fn(), + }; }); test('renders ControlsTab', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); // eslint-disable-line + const component = shallowWithIntl(); + + expect(component).toMatchSnapshot(); }); describe('behavior', () => { test('add control button', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); + findTestSubject(component, 'inputControlEditorAddBtn').simulate('click'); - // Use custom match function since control.id is dynamically generated and never the same. - sinon.assert.calledWith(stageEditorParams, sinon.match((newParams) => { - if (newParams.controls.length !== 3) { - return false; - } - return true; - }, 'control not added to editorState.params')); + + // // Use custom match function since control.id is dynamically generated and never the same. + expect(props.setValue).toHaveBeenCalledWith( + 'controls', + expect.arrayContaining(props.stateParams.controls) + ); + expect(props.setValue.mock.calls[0][1].length).toEqual(3); }); test('remove control button', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); findTestSubject(component, 'inputControlEditorRemoveControl0').simulate('click'); - const expectedParams = { - 'controls': [ - { - 'id': '2', - 'indexPattern': 'indexPattern1', - 'fieldName': 'numberField', - 'label': '', - 'type': 'range', - 'options': { - 'step': 1 - } - } - ] - }; - sinon.assert.calledWith(stageEditorParams, sinon.match(expectedParams)); + const expectedParams = ['controls', [{ + 'id': '2', + 'indexPattern': 'indexPattern1', + 'fieldName': 'numberField', + 'label': '', + 'type': 'range', + 'options': { + 'step': 1 + } + }]]; + + expect(props.setValue).toHaveBeenCalledWith(...expectedParams); }); test('move down control button', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); findTestSubject(component, 'inputControlEditorMoveDownControl0').simulate('click'); - const expectedParams = { - 'controls': [ - { - 'id': '2', - 'indexPattern': 'indexPattern1', - 'fieldName': 'numberField', - 'label': '', - 'type': 'range', - 'options': { - 'step': 1 - } - }, - { - 'id': '1', - 'indexPattern': 'indexPattern1', - 'fieldName': 'keywordField', - 'label': 'custom label', - 'type': 'list', - 'options': { - 'type': 'terms', - 'multiselect': true, - 'size': 5, - 'order': 'desc' - } + const expectedParams = ['controls', [ + { + 'id': '2', + 'indexPattern': 'indexPattern1', + 'fieldName': 'numberField', + 'label': '', + 'type': 'range', + 'options': { + 'step': 1 } - ] - }; - sinon.assert.calledWith(stageEditorParams, sinon.match(expectedParams)); + }, + { + 'id': '1', + 'indexPattern': 'indexPattern1', + 'fieldName': 'keywordField', + 'label': 'custom label', + 'type': 'list', + 'options': { + 'type': 'terms', + 'multiselect': true, + 'size': 5, + 'order': 'desc' + } + } + ]]; + + expect(props.setValue).toHaveBeenCalledWith(...expectedParams); }); test('move up control button', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); findTestSubject(component, 'inputControlEditorMoveUpControl1').simulate('click'); - const expectedParams = { - 'controls': [ - { - 'id': '2', - 'indexPattern': 'indexPattern1', - 'fieldName': 'numberField', - 'label': '', - 'type': 'range', - 'options': { - 'step': 1 - } - }, - { - 'id': '1', - 'indexPattern': 'indexPattern1', - 'fieldName': 'keywordField', - 'label': 'custom label', - 'type': 'list', - 'options': { - 'type': 'terms', - 'multiselect': true, - 'size': 5, - 'order': 'desc' - } + const expectedParams = ['controls', [ + { + 'id': '2', + 'indexPattern': 'indexPattern1', + 'fieldName': 'numberField', + 'label': '', + 'type': 'range', + 'options': { + 'step': 1 } - ] - }; - sinon.assert.calledWith(stageEditorParams, sinon.match(expectedParams)); + }, + { + 'id': '1', + 'indexPattern': 'indexPattern1', + 'fieldName': 'keywordField', + 'label': 'custom label', + 'type': 'list', + 'options': { + 'type': 'terms', + 'multiselect': true, + 'size': 5, + 'order': 'desc' + } + } + ]]; + + expect(props.setValue).toHaveBeenCalledWith(...expectedParams); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js index 449f5594f854..663a36ab69f4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { injectI18n } from '@kbn/i18n/react'; -import { IndexPatternSelect } from 'ui/index_patterns/components/index_pattern_select'; +import { IndexPatternSelect } from 'ui/index_patterns'; import { EuiFormRow, diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js index f4adcbf5cb7e..6121a509e6f3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js @@ -17,6 +17,8 @@ * under the License. */ +jest.mock('ui/new_platform'); + import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js index d63ef6611785..0045ec2508b9 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js @@ -17,7 +17,6 @@ * under the License. */ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; @@ -31,22 +30,16 @@ import { FormattedMessage } from '@kbn/i18n/react'; export class OptionsTab extends Component { - setVisParam = (paramName, paramValue) => { - const params = _.cloneDeep(this.props.editorState.params); - params[paramName] = paramValue; - this.props.stageEditorParams(params); - } - handleUpdateFiltersChange = (evt) => { - this.setVisParam('updateFiltersOnChange', evt.target.checked); + this.props.setValue('updateFiltersOnChange', evt.target.checked); } handleUseTimeFilter = (evt) => { - this.setVisParam('useTimeFilter', evt.target.checked); + this.props.setValue('useTimeFilter', evt.target.checked); } handlePinFilters = (evt) => { - this.setVisParam('pinFilters', evt.target.checked); + this.props.setValue('pinFilters', evt.target.checked); } render() { @@ -60,7 +53,7 @@ export class OptionsTab extends Component { id="inputControl.editor.optionsTab.updateFilterLabel" defaultMessage="Update Kibana filters on each change" />} - checked={this.props.editorState.params.updateFiltersOnChange} + checked={this.props.stateParams.updateFiltersOnChange} onChange={this.handleUpdateFiltersChange} data-test-subj="inputControlEditorUpdateFiltersOnChangeCheckbox" /> @@ -74,7 +67,7 @@ export class OptionsTab extends Component { id="inputControl.editor.optionsTab.useTimeFilterLabel" defaultMessage="Use time filter" />} - checked={this.props.editorState.params.useTimeFilter} + checked={this.props.stateParams.useTimeFilter} onChange={this.handleUseTimeFilter} data-test-subj="inputControlEditorUseTimeFilterCheckbox" /> @@ -88,7 +81,7 @@ export class OptionsTab extends Component { id="inputControl.editor.optionsTab.pinFiltersLabel" defaultMessage="Pin filters for all applications" />} - checked={this.props.editorState.params.pinFilters} + checked={this.props.stateParams.pinFilters} onChange={this.handlePinFilters} data-test-subj="inputControlEditorPinFiltersCheckbox" /> @@ -99,6 +92,6 @@ export class OptionsTab extends Component { } OptionsTab.propTypes = { - scope: PropTypes.object.isRequired, - stageEditorParams: PropTypes.func.isRequired + vis: PropTypes.object.isRequired, + setValue: PropTypes.func.isRequired }; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js index ba4d43bea133..39f5f6a50a5a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js @@ -18,7 +18,6 @@ */ import React from 'react'; -import sinon from 'sinon'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -26,70 +25,51 @@ import { OptionsTab, } from './options_tab'; -const scopeMock = { - editorState: { - params: { - updateFiltersOnChange: false, - useTimeFilter: false - } - } -}; -let stageEditorParams; +describe('OptionsTab', () => { + let props; -beforeEach(() => { - stageEditorParams = sinon.spy(); -}); + beforeEach(() => { + props = { + vis: {}, + stateParams: { + updateFiltersOnChange: false, + useTimeFilter: false + }, + setValue: jest.fn() + }; + }); -test('renders OptionsTab', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line -}); + it('should renders OptionsTab', () => { + const component = shallow(); -test('updateFiltersOnChange', () => { - const component = mountWithIntl(); - const checkbox = component.find('[data-test-subj="inputControlEditorUpdateFiltersOnChangeCheckbox"] input[type="checkbox"]'); - checkbox.simulate('change', { target: { checked: true } }); - const expectedParams = { - updateFiltersOnChange: true - }; - sinon.assert.calledOnce(stageEditorParams); - sinon.assert.calledWith(stageEditorParams, sinon.match(expectedParams)); -}); + expect(component).toMatchSnapshot(); + }); -test('useTimeFilter', () => { - const component = mountWithIntl(); - const checkbox = component.find('[data-test-subj="inputControlEditorUseTimeFilterCheckbox"] input[type="checkbox"]'); - checkbox.simulate('change', { target: { checked: true } }); - const expectedParams = { - useTimeFilter: true - }; - sinon.assert.calledOnce(stageEditorParams); - sinon.assert.calledWith(stageEditorParams, sinon.match(expectedParams)); -}); + it('should update updateFiltersOnChange', () => { + const component = mountWithIntl(); + const checkbox = component.find('[data-test-subj="inputControlEditorUpdateFiltersOnChangeCheckbox"] input[type="checkbox"]'); + checkbox.simulate('change', { target: { checked: true } }); + + expect(props.setValue).toHaveBeenCalledTimes(1); + expect(props.setValue).toHaveBeenCalledWith('updateFiltersOnChange', true); + }); + + it('should update useTimeFilter', () => { + const component = mountWithIntl(); + const checkbox = component.find('[data-test-subj="inputControlEditorUseTimeFilterCheckbox"] input[type="checkbox"]'); + checkbox.simulate('change', { target: { checked: true } }); + + expect(props.setValue).toHaveBeenCalledTimes(1); + expect(props.setValue).toHaveBeenCalledWith('useTimeFilter', true); + }); + + it('should update pinFilters', () => { + const component = mountWithIntl(); + const checkbox = component.find('[data-test-subj="inputControlEditorPinFiltersCheckbox"] input[type="checkbox"]'); + checkbox.simulate('change', { target: { checked: true } }); + + expect(props.setValue).toHaveBeenCalledTimes(1); + expect(props.setValue).toHaveBeenCalledWith('pinFilters', true); + }); -test('pinFilters', () => { - const component = mountWithIntl(); - const checkbox = component.find('[data-test-subj="inputControlEditorPinFiltersCheckbox"] input[type="checkbox"]'); - checkbox.simulate('change', { target: { checked: true } }); - const expectedParams = { - pinFilters: true - }; - sinon.assert.calledOnce(stageEditorParams); - sinon.assert.calledWith(stageEditorParams, sinon.match(expectedParams)); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js index f689248e751f..4d8e4fbdf8dd 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js @@ -17,6 +17,8 @@ * under the License. */ +jest.mock('ui/new_platform'); + import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.js.snap index efd58cdaff54..2e6c1058e160 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.js.snap @@ -9,7 +9,9 @@ exports[`disabled 1`] = ` > @@ -24,6 +26,8 @@ exports[`renders RangeControl 1`] = ` > - +

    + +

    + + } + iconColor="subdued" + title={ +

    + +

    + } > - -
    -
    - -

    - -

    - - } - iconColor="subdued" - title={ -

    - -

    - } + +

    + + No data available + +

    +
    +
    + + +
    - - + - -

    - - No data available - -

    -
    - -
    - - -
    -

    - - The element did not provide any data. - -

    -
    -
    - - + The element did not provide any data. + +

    - -
    -
    - - + + + +
    + `; @@ -328,91 +312,85 @@ exports[`Inspector Data View component should render loading state 1`] = ` } title="Test Data" > - - -
    -
    - + + + + + + + +
    + + +
    - -
    + - - - - - - - - - -
    - - -
    -

    - - Gathering data - -

    -
    -
    -
    - + Gathering data +
    +

    - +
    - +
    -
    - - + +
    + `; diff --git a/src/legacy/core_plugins/inspector_views/public/data/data_view.js b/src/legacy/core_plugins/inspector_views/public/data/data_view.js index 32c340f110fc..265e4dde2963 100644 --- a/src/legacy/core_plugins/inspector_views/public/data/data_view.js +++ b/src/legacy/core_plugins/inspector_views/public/data/data_view.js @@ -29,8 +29,6 @@ import { EuiText, } from '@elastic/eui'; -import { InspectorView } from 'ui/inspector'; - import { DataTableFormat, } from './data_table'; @@ -102,54 +100,51 @@ class DataViewComponent extends Component { renderNoData() { return ( - - + + + + } + body={ + +

    - - } - body={ - -

    - -

    -
    - } - /> -
    +

    + + } + /> ); } renderLoading() { return ( - - - - - - - -

    - -

    -
    -
    -
    -
    -
    + + + + + + +

    + +

    +
    +
    +
    +
    ); } @@ -161,13 +156,11 @@ class DataViewComponent extends Component { } return ( - - - + ); } } diff --git a/src/legacy/core_plugins/inspector_views/public/data/data_view.test.js b/src/legacy/core_plugins/inspector_views/public/data/data_view.test.js index 29d536d60e83..c616a7504d3e 100644 --- a/src/legacy/core_plugins/inspector_views/public/data/data_view.test.js +++ b/src/legacy/core_plugins/inspector_views/public/data/data_view.test.js @@ -22,6 +22,7 @@ import { DataView } from './data_view'; import { DataAdapter } from 'ui/inspector/adapters'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +jest.mock('ui/new_platform'); jest.mock('./lib/export_csv', () => ({ exportAsCsv: jest.fn(), })); diff --git a/src/legacy/core_plugins/inspector_views/public/requests/requests_view.js b/src/legacy/core_plugins/inspector_views/public/requests/requests_view.js index 2ce47f62ef95..a949b2ebe6ea 100644 --- a/src/legacy/core_plugins/inspector_views/public/requests/requests_view.js +++ b/src/legacy/core_plugins/inspector_views/public/requests/requests_view.js @@ -26,7 +26,6 @@ import { EuiTextColor, } from '@elastic/eui'; -import { InspectorView } from 'ui/inspector'; import { RequestStatus } from 'ui/inspector/adapters'; import { RequestSelector } from './request_selector'; @@ -68,36 +67,34 @@ class RequestsViewComponent extends Component { renderEmptyRequests() { return ( - - + + + + } + body={ + +

    - - } - body={ - -

    - -

    -

    - -

    -
    - } - /> -
    +

    +

    + +

    + + } + /> ); } @@ -111,7 +108,7 @@ class RequestsViewComponent extends Component { ).length; return ( - + <>

    } - + ); } } diff --git a/src/legacy/core_plugins/kbn_doc_views/public/__tests__/doc_views.js b/src/legacy/core_plugins/kbn_doc_views/public/__tests__/doc_views.js index 298cddede6cb..dfd0b39bb20a 100644 --- a/src/legacy/core_plugins/kbn_doc_views/public/__tests__/doc_views.js +++ b/src/legacy/core_plugins/kbn_doc_views/public/__tests__/doc_views.js @@ -22,7 +22,7 @@ import _ from 'lodash'; import sinon from 'sinon'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import 'ui/render_directive'; +import 'ui/directives/render_directive'; import '../views/table'; import { DocViewsRegistryProvider } from 'ui/registry/doc_views'; import StubbedLogstashIndexPattern from 'fixtures/stubbed_logstash_index_pattern'; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/basic_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/basic_options.tsx new file mode 100644 index 000000000000..ed9c6445dc23 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/basic_options.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { VisOptionsProps } from 'ui/vis/editors/default'; +import { SwitchOption } from './switch'; +import { SelectOption } from './select'; + +export interface BasicOptionsParams { + addTooltip: boolean; + legendPosition: string; +} + +function BasicOptions({ + stateParams, + setValue, + vis, +}: VisOptionsProps) { + return ( + <> + + + + ); +} + +export { BasicOptions }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/range.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/range.tsx new file mode 100644 index 000000000000..af21297e018f --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/range.tsx @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { EuiFormRow, EuiRange } from '@elastic/eui'; + +interface RangeOptionProps { + label: string; + max: number; + min: number; + paramName: ParamName; + step?: number; + value: '' | number; + setValue: (paramName: ParamName, value: number) => void; +} + +function RangeOption({ + label, + max, + min, + step, + paramName, + value, + setValue, +}: RangeOptionProps) { + return ( + + setValue(paramName, ev.target.valueAsNumber)} + /> + + ); +} + +export { RangeOption }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/select.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/select.tsx new file mode 100644 index 000000000000..01c6a576d4d1 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/select.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 from 'react'; +import { EuiFormRow, EuiSelect } from '@elastic/eui'; + +interface SelectOptionProps { + label: string; + options: Array<{ value: ValidParamValues; text: string }>; + paramName: ParamName; + value?: ValidParamValues; + setValue: (paramName: ParamName, value: ValidParamValues) => void; +} + +function SelectOption({ + label, + options, + paramName, + value, + setValue, +}: SelectOptionProps) { + return ( + + setValue(paramName, ev.target.value as ValidParamValues)} + fullWidth={true} + /> + + ); +} + +export { SelectOption }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/switch.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/switch.tsx new file mode 100644 index 000000000000..a1ef887a8bd5 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/switch.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { EuiSwitch, EuiToolTip } from '@elastic/eui'; + +interface SwitchOptionProps { + dataTestSubj?: string; + label?: string; + tooltip?: string; + disabled?: boolean; + value?: boolean; + paramName: ParamName; + setValue: (paramName: ParamName, value: boolean) => void; +} + +function SwitchOption({ + dataTestSubj, + tooltip, + label, + disabled, + paramName, + value = false, + setValue, +}: SwitchOptionProps) { + return ( +

    + + setValue(paramName, ev.target.checked)} + /> + +
    + ); +} + +export { SwitchOption }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/text_input.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/text_input.tsx new file mode 100644 index 000000000000..a25b77878f75 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/text_input.tsx @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { EuiFormRow, EuiFieldText } from '@elastic/eui'; + +interface TextInputOptionProps { + helpText?: React.ReactNode; + label?: React.ReactNode; + paramName: ParamName; + value?: string; + setValue: (paramName: ParamName, value: string) => void; +} + +function TextInputOption({ + helpText, + label, + paramName, + value = '', + setValue, +}: TextInputOptionProps) { + return ( + + setValue(paramName, ev.target.value)} /> + + ); +} + +export { TextInputOption }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/truncate_labels.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/truncate_labels.tsx new file mode 100644 index 000000000000..9e5c25783d49 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/truncate_labels.tsx @@ -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 React, { ChangeEvent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiFieldNumber } from '@elastic/eui'; + +interface TruncateLabelsOptionProps { + value: number | null; + setValue: (paramName: 'truncate', value: null | number) => void; +} + +function TruncateLabelsOption({ value, setValue }: TruncateLabelsOptionProps) { + const onChange = (ev: ChangeEvent) => + setValue('truncate', ev.target.value === '' ? null : parseFloat(ev.target.value)); + + return ( + + + + ); +} + +export { TruncateLabelsOption }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/vislib_basic_options.html b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/vislib_basic_options.html deleted file mode 100644 index 922b7b75c645..000000000000 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/vislib_basic_options.html +++ /dev/null @@ -1,31 +0,0 @@ -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/vislib_basic_options.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/vislib_basic_options.js deleted file mode 100644 index 5823218d3bde..000000000000 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/vislib_basic_options.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 { uiModules } from 'ui/modules'; -import vislibBasicOptionsTemplate from './vislib_basic_options.html'; -const module = uiModules.get('kibana'); - -module.directive('vislibBasicOptions', function () { - return { - restrict: 'E', - template: vislibBasicOptionsTemplate, - replace: true - }; -}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/pie.html b/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/pie.html deleted file mode 100644 index cd1a6f85d27f..000000000000 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/pie.html +++ /dev/null @@ -1,92 +0,0 @@ -
    -
    -
    -
    -
    - -
    - -
    - -
    -
    -
    - - -
    - - -
    -
    -
    -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    -
    diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/pie.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/pie.tsx new file mode 100644 index 000000000000..85d3ac8ce1b1 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/editors/pie.tsx @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { VisOptionsProps } from 'ui/vis/editors/default'; +import { BasicOptions } from '../controls/basic_options'; +import { SwitchOption } from '../controls/switch'; +import { TruncateLabelsOption } from '../controls/truncate_labels'; +import { PieVisParams } from '../pie'; + +function PieOptions(props: VisOptionsProps) { + const { stateParams, setValue } = props; + const setLabels = ( + paramName: T, + value: PieVisParams['labels'][T] + ) => setValue('labels', { ...stateParams.labels, [paramName]: value }); + + return ( + <> + + +
    + +
    +
    + + + +
    + + + + + +
    + +
    +
    + + + + + +
    + + ); +} + +export { PieOptions }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.d.ts b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.d.ts new file mode 100644 index 000000000000..89dbe03b2ea2 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.d.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { CommonVislibParams } from './types'; + +export interface PieVisParams extends CommonVislibParams { + addLegend: boolean; + isDonut: boolean; + labels: { + show: boolean; + values: boolean; + last_level: boolean; + truncate: number | null; + }; +} diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js index e8b8a969bfb3..85727989f098 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js @@ -20,7 +20,7 @@ import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; -import pieTemplate from './editors/pie.html'; +import { PieOptions } from './editors/pie'; export default function HistogramVisType(Private) { const VisFactory = Private(VisFactoryProvider); @@ -50,21 +50,34 @@ export default function HistogramVisType(Private) { }, editorConfig: { collections: { - legendPositions: [{ - value: 'left', - text: 'left', - }, { - value: 'right', - text: 'right', - }, { - value: 'top', - text: 'top', - }, { - value: 'bottom', - text: 'bottom', - }], + legendPositions: [ + { + text: i18n.translate('kbnVislibVisTypes.pie.editorConfig.legendPositions.leftText', { + defaultMessage: 'Left' + }), + value: 'left' + }, + { + text: i18n.translate('kbnVislibVisTypes.pie.editorConfig.legendPositions.rightText', { + defaultMessage: 'Right' + }), + value: 'right' + }, + { + text: i18n.translate('kbnVislibVisTypes.pie.editorConfig.legendPositions.topText', { + defaultMessage: 'Top' + }), + value: 'top' + }, + { + text: i18n.translate('kbnVislibVisTypes.pie.editorConfig.legendPositions.bottomText', { + defaultMessage: 'Bottom' + }), + value: 'bottom' + }, + ], }, - optionsTemplate: pieTemplate, + optionsTemplate: PieOptions, schemas: new Schemas([ { group: 'metrics', diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/types.ts b/src/legacy/core_plugins/kbn_vislib_vis_types/public/types.ts new file mode 100644 index 000000000000..8bf941d8b945 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/types.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 CommonVislibParams { + addTooltip: boolean; + legendPosition: 'right' | 'left' | 'top' | 'bottom'; +} diff --git a/src/legacy/core_plugins/kibana/common/tutorials/functionbeat_instructions.js b/src/legacy/core_plugins/kibana/common/tutorials/functionbeat_instructions.js index 8c1135ce1215..a62c9707026b 100644 --- a/src/legacy/core_plugins/kibana/common/tutorials/functionbeat_instructions.js +++ b/src/legacy/core_plugins/kibana/common/tutorials/functionbeat_instructions.js @@ -88,7 +88,7 @@ Kibana index pattern. It is normally safe to omit this command.', }), commands: [ './functionbeat setup', - './functionbeat deploy fn_cloudwatch_logs', + './functionbeat deploy fn-cloudwatch-logs', ] }, WINDOWS: { @@ -102,7 +102,7 @@ Kibana index pattern. It is normally safe to omit this command.', }), commands: [ '.\\functionbeat.exe setup', - '.\\functionbeat.exe deploy fn_cloudwatch_logs', + '.\\functionbeat.exe deploy fn-cloudwatch-logs', ], }, }, @@ -217,7 +217,7 @@ export function functionbeatEnableInstructions() { }); const defaultCommands = [ 'functionbeat.provider.aws.functions:', - ' - name: fn_cloudwatch_logs', + ' - name: fn-cloudwatch-logs', ' enabled: true', ' type: cloudwatch_logs', ' triggers:', diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 2ff4d1bdf9d4..0b3aedee7a7a 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -235,9 +235,22 @@ export default function (kibana) { }, injectDefaultVars(server, options) { + const mapConfig = server.config().get('map'); + const tilemap = mapConfig.tilemap; + return { kbnIndex: options.index, kbnBaseUrl, + + // required on all pages due to hacks that use these values + mapConfig, + tilemapsConfig: { + deprecated: { + // If url is set, old settings must be used for backward compatibility + isOverridden: typeof tilemap.url === 'string' && tilemap.url !== '', + config: tilemap, + }, + }, }; }, diff --git a/src/legacy/core_plugins/kibana/inject_vars.js b/src/legacy/core_plugins/kibana/inject_vars.js index c91d1b7214cd..4bf11f28732e 100644 --- a/src/legacy/core_plugins/kibana/inject_vars.js +++ b/src/legacy/core_plugins/kibana/inject_vars.js @@ -19,12 +19,6 @@ export function injectVars(server) { const serverConfig = server.config(); - const mapConfig = serverConfig.get('map'); - const regionmap = mapConfig.regionmap; - const tilemap = mapConfig.tilemap; - - // If url is set, old settings must be used for backward compatibility - const isOverridden = typeof tilemap.url === 'string' && tilemap.url !== ''; // Get types that are import and exportable, by default yes unless isImportableAndExportable is set to false const { types: allTypes } = server.savedObjects; @@ -36,16 +30,8 @@ export function injectVars(server) { return { kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'), disableWelcomeScreen: serverConfig.get('kibana.disableWelcomeScreen'), - regionmapsConfig: regionmap, - mapConfig: mapConfig, importAndExportableTypes, autocompleteTerminateAfter: serverConfig.get('kibana.autocompleteTerminateAfter'), autocompleteTimeout: serverConfig.get('kibana.autocompleteTimeout'), - tilemapsConfig: { - deprecated: { - isOverridden: isOverridden, - config: tilemap, - }, - }, }; } diff --git a/src/legacy/core_plugins/kibana/public/context/api/context.ts b/src/legacy/core_plugins/kibana/public/context/api/context.ts index 28bf73ecfd6d..be99fa63b07c 100644 --- a/src/legacy/core_plugins/kibana/public/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/context/api/context.ts @@ -20,8 +20,8 @@ // @ts-ignore import { SearchSourceProvider, SearchSource } from 'ui/courier'; import { IPrivate } from 'ui/private'; -import { IndexPatternEnhanced, IndexPatternGetProvider } from 'ui/index_patterns/_index_pattern'; import { Filter } from '@kbn/es-query'; +import { IndexPatterns, IndexPattern } from 'ui/index_patterns'; import { reverseSortDir, SortDirection } from './utils/sorting'; import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; @@ -42,7 +42,7 @@ const DAY_MILLIS = 24 * 60 * 60 * 1000; // look from 1 day up to 10000 days into the past and future const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000].map(days => days * DAY_MILLIS); -function fetchContextProvider(indexPatterns: IndexPatternGetProvider, Private: IPrivate) { +function fetchContextProvider(indexPatterns: IndexPatterns, Private: IPrivate) { const SearchSourcePrivate: any = Private(SearchSourceProvider); return { @@ -112,7 +112,7 @@ function fetchContextProvider(indexPatterns: IndexPatternGetProvider, Private: I return documents; } - async function createSearchSource(indexPattern: IndexPatternEnhanced, filters: Filter[]) { + async function createSearchSource(indexPattern: IndexPattern, filters: Filter[]) { return new SearchSourcePrivate() .setParent(false) .setField('index', indexPattern) diff --git a/src/legacy/core_plugins/kibana/public/context/app.js b/src/legacy/core_plugins/kibana/public/context/app.js index 243a58a63b45..8a7de39d402c 100644 --- a/src/legacy/core_plugins/kibana/public/context/app.js +++ b/src/legacy/core_plugins/kibana/public/context/app.js @@ -38,8 +38,8 @@ import { } from './query'; import { timefilter } from 'ui/timefilter'; -import { data } from 'plugins/data/setup'; -data.filter.loadLegacyDirectives(); +// load directives +import '../../../data/public/legacy'; const module = uiModules.get('apps/context', [ 'elasticsearch', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts index 81a894985c57..eac9c18670c8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts @@ -49,6 +49,7 @@ describe('DashboardState', function() { isAutoRefreshSelectorEnabled: true, isTimeRangeSelectorEnabled: true, }; + function initDashboardState() { dashboardState = new DashboardStateManager({ savedDashboard, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.js b/src/legacy/core_plugins/kibana/public/dashboard/index.js index 6c97d4f86478..3eff43db2fd9 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.js @@ -39,8 +39,8 @@ import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { uiModules } from 'ui/modules'; import 'ui/capabilities/route_setup'; -import { data } from 'plugins/data/setup'; -data.filter.loadLegacyDirectives(); +// load directives +import '../../../data/public'; const app = uiModules.get('app/dashboard', [ 'ngRoute', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts index f18a1b29f718..caa262b899ac 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts @@ -34,6 +34,8 @@ jest.mock( { virtual: true } ); +jest.mock('ui/new_platform'); + import { migratePanelsTo730 } from './migrate_to_730_panels'; import { SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest } from '../types'; import { @@ -301,6 +303,28 @@ test('6.1 migrates uiState, sort, and scales', async () => { expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)'); }); +// https://github.com/elastic/kibana/issues/42519 +it('6.1 migrates when uiState={} and panels have sort / column override', () => { + const uiState = {}; + const panels: RawSavedDashboardPanel610[] = [ + { + panelIndex: 1, + sort: 'sort', + version: '6.1.0', + name: 'panel-123', + gridData: { h: 3, x: 0, y: 0, w: 6, i: '123' }, + }, + { + panelIndex: 2, + columns: ['hi'], + version: '6.1.0', + name: 'panel-123', + gridData: { h: 3, x: 0, y: 0, w: 6, i: '123' }, + }, + ]; + expect(() => migratePanelsTo730(panels, '8.0.0', true, uiState)).not.toThrow(); +}); + test('6.2 migrates sort and scales', async () => { const panels: RawSavedDashboardPanel620[] = [ { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts index b52602df1b8b..5e618956a760 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts @@ -161,7 +161,7 @@ function migrate610PanelToLatest( } }); - const embeddableConfig = uiState ? uiState[`P-${panel.panelIndex}`] : {}; + const embeddableConfig = uiState ? uiState[`P-${panel.panelIndex}`] || {} : {}; // 2. (6.4) remove columns, sort properties if (panel.columns || panel.sort) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts index ee28643f3644..5300811a6dab 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { Logger } from 'target/types/server/saved_objects/migrations/core/migration_logger'; +import { SavedObjectsMigrationLogger } from 'src/core/server'; +import { inspect } from 'util'; import { DashboardDoc730ToLatest, DashboardDoc700To720 } from './types'; import { isDashboardDoc } from './is_dashboard_doc'; import { moveFiltersToQuery } from './move_filters_to_query'; @@ -28,7 +29,7 @@ export function migrations730( [key: string]: unknown; } | DashboardDoc700To720, - logger: Logger + logger: SavedObjectsMigrationLogger ): DashboardDoc730ToLatest | { [key: string]: unknown } { if (!isDashboardDoc(doc)) { // NOTE: we should probably throw an error here... but for now following suit and in the @@ -43,7 +44,9 @@ export function migrations730( ); } catch (e) { logger.warning( - `Exception @ migrations730 while trying to migrate query filters!\nError:${e}\nSearchSource JSON:\n${doc.attributes.kibanaSavedObjectMeta.searchSourceJSON}` + `Exception @ migrations730 while trying to migrate dashboard query filters!\n` + + `${e.stack}\n` + + `dashboard: ${inspect(doc, false, null)}` ); return doc; } @@ -67,7 +70,11 @@ export function migrations730( delete doc.attributes.uiStateJSON; } catch (e) { - logger.warning(`Exception @ migrations730 while trying to migrate dashboard panels! ${e}`); + logger.warning( + `Exception @ migrations730 while trying to migrate dashboard panels!\n` + + `Error: ${e.stack}\n` + + `dashboard: ${inspect(doc, false, null)}` + ); return doc; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts index fff5aeab599e..e1d9cfac9526 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts @@ -17,35 +17,11 @@ * under the License. */ -import { fatalErrorsServiceMock, notificationServiceMock } from '../../../../../core/public/mocks'; - let modalContents: React.Component; export const getModalContents = () => modalContents; -jest.doMock('ui/new_platform', () => { - return { - npStart: { - core: { - overlays: { - openFlyout: jest.fn(), - openModal: (component: React.Component) => { - modalContents = component; - return { - close: jest.fn(), - }; - }, - }, - }, - }, - npSetup: { - core: { - fatalErrors: fatalErrorsServiceMock.createSetupContract(), - notifications: notificationServiceMock.createSetupContract(), - }, - }, - }; -}); +jest.mock('ui/new_platform'); jest.doMock('ui/metadata', () => ({ metadata: { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index f76a0ab72b21..e316dd3254cc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -27,7 +27,7 @@ import 'ui/private'; import '../../components/field_chooser/field_chooser'; import FixturesHitsProvider from 'fixtures/hits'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { SimpleSavedObject } from 'ui/saved_objects'; +import { SimpleSavedObject } from '../../../../../../../core/public'; // Load the kibana app dependencies. diff --git a/src/legacy/core_plugins/kibana/public/discover/_discover.scss b/src/legacy/core_plugins/kibana/public/discover/_discover.scss index 2e142d475be2..b625e394a138 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_discover.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_discover.scss @@ -1,5 +1,11 @@ +@import 'node_modules/@elastic/eui/src/components/panel/mixins'; + discover-app { - background-color: $euiColorEmptyShade; + flex-grow: 1; + + .sidebar-container { + background-color: transparent; + } } // SASSTODO: replace the margin-top value with a variable @@ -14,14 +20,20 @@ discover-app { // SASSTODO: replace the z-index value with a variable .dscWrapper { - padding-right: 0; + padding-right: $euiSizeS; padding-left: 21px; - z-index: 1 + z-index: 1; } +@include euiPanel('dscWrapper__content'); + .dscWrapper__content { - padding-right: $euiSize; - clear: both; + padding-top: $euiSizeXS; + background-color: $euiColorEmptyShade; + + .kbn-table { + margin-bottom: 0; + } } .dscTimechart { @@ -41,11 +53,11 @@ discover-app { padding-left: $euiSizeM; .dscResultHits { - padding-left: $euiSizeXS; + padding-left: $euiSizeXS; } > .kuiLink { - padding-left: $euiSizeM; + padding-left: $euiSizeM; } } @@ -136,7 +148,7 @@ discover-app { // SASSTODO: replace the padding value with a variable .dscFieldDetails { padding: 10px; - background-color: shade($euiColorLightestShade, 5%); + background-color: $euiColorLightestShade; color: $euiTextColor; } diff --git a/src/legacy/core_plugins/kibana/public/discover/_hacks.scss b/src/legacy/core_plugins/kibana/public/discover/_hacks.scss index 7f2b5b340004..cdc8e04dff57 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_hacks.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_hacks.scss @@ -1,7 +1,6 @@ -// SASSTODO: the classname is dinamically generated with ng-class +// SASSTODO: the classname is dynamically generated with ng-class .tab-discover { overflow: hidden; - background: $euiColorEmptyShade; } // SASSTODO: these are Angular Bootstrap classes. Will be replaced by EUI diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js index 15a49cb9d962..1336a7351c04 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.js @@ -26,7 +26,7 @@ import _ from 'lodash'; import $ from 'jquery'; import rison from 'rison-node'; import { fieldCalculator } from './lib/field_calculator'; -import { FieldList } from 'ui/index_patterns/_field_list'; +import { FieldList } from 'ui/index_patterns'; import { uiModules } from 'ui/modules'; import fieldChooserTemplate from './field_chooser.html'; const app = uiModules.get('apps/discover'); diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 94a62511bc2a..377fd72e9c77 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -31,7 +31,7 @@ import { getSort } from '../doc_table/lib/get_sort'; import * as columnActions from '../doc_table/actions/columns'; import * as filterActions from '../doc_table/actions/filter'; -import 'ui/listen'; +import 'ui/directives/listen'; import 'ui/visualize'; import 'ui/fixed_scroll'; import 'ui/index_patterns'; @@ -530,6 +530,14 @@ function discoverController( indexPatternList: $route.current.locals.ip.list, }; + const shouldSearchOnPageLoad = () => { + // A saved search is created on every page load, so we check the ID to see if we're loading a + // previously saved search or if it is just transient + return config.get('discover:searchOnPageLoad') + || savedSearch.id !== undefined + || _.get($scope, 'refreshInterval.pause') === false; + }; + const init = _.once(function () { stateMonitor = stateMonitorFactory.create($state, getStateDefaults()); stateMonitor.onChange((status) => { @@ -546,8 +554,8 @@ function discoverController( $scope.$watchCollection('state.sort', function (sort) { if (!sort) return; - // get the current sort from {key: val} to ["key", "val"]; - const currentSort = _.pairs($scope.searchSource.getField('sort')).pop(); + // get the current sort from searchSource as array of arrays + const currentSort = getSort.array($scope.searchSource.getField('sort'), $scope.indexPattern); // if the searchSource doesn't know, tell it so if (!angular.equals(sort, currentSort)) $scope.fetch(); @@ -577,8 +585,10 @@ function discoverController( $scope.enableTimeRangeSelector = !!timefield; }); - $scope.$watch('state.interval', function () { - $scope.fetch(); + $scope.$watch('state.interval', function (newInterval, oldInterval) { + if (newInterval !== oldInterval) { + $scope.fetch(); + } }); $scope.$watch('vis.aggs', function () { @@ -592,9 +602,11 @@ function discoverController( } }); - $scope.$watch('state.query', (newQuery) => { - const query = migrateLegacyQuery(newQuery); - $scope.updateQueryAndFetch({ query }); + $scope.$watch('state.query', (newQuery, oldQuery) => { + if (!_.isEqual(newQuery, oldQuery)) { + const query = migrateLegacyQuery(newQuery); + $scope.updateQueryAndFetch({ query }); + } }); $scope.$watchMulti([ @@ -603,19 +615,25 @@ function discoverController( ], (function updateResultState() { let prev = {}; const status = { + UNINITIALIZED: 'uninitialized', LOADING: 'loading', // initial data load READY: 'ready', // results came back NO_RESULTS: 'none' // no results came back }; function pick(rows, oldRows, fetchStatus) { - // initial state, pretend we are loading - if (rows == null && oldRows == null) return status.LOADING; + // initial state, pretend we're already loading if we're about to execute a search so + // that the uninitilized message doesn't flash on screen + if (rows == null && oldRows == null && shouldSearchOnPageLoad()) { + return status.LOADING; + } + + if (fetchStatus === fetchStatuses.UNINITIALIZED) { + return status.UNINITIALIZED; + } const rowsEmpty = _.isEmpty(rows); - const preparingForFetch = fetchStatus === fetchStatuses.UNINITIALIZED; - if (preparingForFetch) return status.LOADING; - else if (rowsEmpty && fetchStatus === fetchStatuses.LOADING) return status.LOADING; + if (rowsEmpty && fetchStatus === fetchStatuses.LOADING) return status.LOADING; else if (!rowsEmpty) return status.READY; else return status.NO_RESULTS; } @@ -644,6 +662,10 @@ function discoverController( init.complete = true; $state.replace(); + + if (shouldSearchOnPageLoad()) { + $scope.fetch(); + } }); }); @@ -807,7 +829,14 @@ function discoverController( }; $scope.updateRefreshInterval = function () { - $scope.refreshInterval = timefilter.getRefreshInterval(); + const newInterval = timefilter.getRefreshInterval(); + const shouldFetch = _.get($scope, 'refreshInterval.pause') === true && newInterval.pause === false; + + $scope.refreshInterval = newInterval; + + if (shouldFetch) { + $scope.fetch(); + } }; $scope.onRefreshChange = function ({ isPaused, refreshInterval }) { @@ -833,8 +862,8 @@ function discoverController( .setField('filter', queryFilter.getFilters()); }); - $scope.setSortOrder = function setSortOrder(columnName, direction) { - $scope.state.sort = [columnName, direction]; + $scope.setSortOrder = function setSortOrder(sortPair) { + $scope.state.sort = sortPair; }; // TODO: On array fields, negating does not negate the combination, rather all terms diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/index.js b/src/legacy/core_plugins/kibana/public/discover/directives/index.js index 30e8bf5a07c2..d13448bbf9c8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/directives/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/directives/index.js @@ -21,21 +21,24 @@ import 'ngreact'; import { wrapInI18nContext } from 'ui/i18n'; import { uiModules } from 'ui/modules'; -import { - DiscoverNoResults, -} from './no_results'; +import { DiscoverNoResults } from './no_results'; -import { - DiscoverUnsupportedIndexPattern, -} from './unsupported_index_pattern'; +import { DiscoverUninitialized } from './uninitialized'; + +import { DiscoverUnsupportedIndexPattern } from './unsupported_index_pattern'; import './timechart'; const app = uiModules.get('apps/discover', ['react']); -app.directive('discoverNoResults', reactDirective => reactDirective(wrapInI18nContext(DiscoverNoResults))); +app.directive('discoverNoResults', reactDirective => + reactDirective(wrapInI18nContext(DiscoverNoResults)) +); + +app.directive('discoverUninitialized', reactDirective => + reactDirective(wrapInI18nContext(DiscoverUninitialized)) +); -app.directive( - 'discoverUnsupportedIndexPattern', - reactDirective => reactDirective(wrapInI18nContext(DiscoverUnsupportedIndexPattern), ['unsupportedType']) +app.directive('discoverUnsupportedIndexPattern', reactDirective => + reactDirective(wrapInI18nContext(DiscoverUnsupportedIndexPattern), ['unsupportedType']) ); diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/uninitialized.tsx b/src/legacy/core_plugins/kibana/public/discover/directives/uninitialized.tsx new file mode 100644 index 000000000000..f40865800098 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/directives/uninitialized.tsx @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiButton, EuiEmptyPrompt, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; + +interface Props { + onRefresh: () => void; +} + +export const DiscoverUninitialized = ({ onRefresh }: Props) => { + return ( + + + + + + + } + body={ +

    + +

    + } + actions={ + + + + } + /> +
    +
    +
    + ); +}; diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js index bec26d38d0fa..b8b962b9f92d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/get_sort.js @@ -23,7 +23,7 @@ import ngMock from 'ng_mock'; import { getSort } from '../../lib/get_sort'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -const defaultSort = { time: 'desc' }; +const defaultSort = [{ time: 'desc' }]; let indexPattern; describe('docTable', function () { @@ -38,11 +38,11 @@ describe('docTable', function () { expect(getSort).to.be.a(Function); }); - it('should return an object if passed a 2 item array', function () { - expect(getSort(['bytes', 'desc'], indexPattern)).to.eql({ bytes: 'desc' }); + it('should return an array of objects if passed a 2 item array', function () { + expect(getSort(['bytes', 'desc'], indexPattern)).to.eql([{ bytes: 'desc' }]); delete indexPattern.timeFieldName; - expect(getSort(['bytes', 'desc'], indexPattern)).to.eql({ bytes: 'desc' }); + expect(getSort(['bytes', 'desc'], indexPattern)).to.eql([{ bytes: 'desc' }]); }); it('should sort by the default when passed an unsortable field', function () { @@ -50,7 +50,7 @@ describe('docTable', function () { expect(getSort(['lol_nope', 'asc'], indexPattern)).to.eql(defaultSort); delete indexPattern.timeFieldName; - expect(getSort(['non-sortable', 'asc'], indexPattern)).to.eql({ _score: 'desc' }); + expect(getSort(['non-sortable', 'asc'], indexPattern)).to.eql([{ _score: 'desc' }]); }); it('should sort in reverse chrono order otherwise on time based patterns', function () { @@ -62,9 +62,9 @@ describe('docTable', function () { it('should sort by score on non-time patterns', function () { delete indexPattern.timeFieldName; - expect(getSort([], indexPattern)).to.eql({ _score: 'desc' }); - expect(getSort(['foo'], indexPattern)).to.eql({ _score: 'desc' }); - expect(getSort({ foo: 'bar' }, indexPattern)).to.eql({ _score: 'desc' }); + expect(getSort([], indexPattern)).to.eql([{ _score: 'desc' }]); + expect(getSort(['foo'], indexPattern)).to.eql([{ _score: 'desc' }]); + expect(getSort({ foo: 'bar' }, indexPattern)).to.eql([{ _score: 'desc' }]); }); }); @@ -73,8 +73,8 @@ describe('docTable', function () { expect(getSort.array).to.be.a(Function); }); - it('should return an array for sortable fields', function () { - expect(getSort.array(['bytes', 'desc'], indexPattern)).to.eql([ 'bytes', 'desc' ]); + it('should return an array of arrays for sortable fields', function () { + expect(getSort.array(['bytes', 'desc'], indexPattern)).to.eql([[ 'bytes', 'desc' ]]); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/rows_headers.js index b2660facf7a5..1081528e2566 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/rows_headers.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/lib/rows_headers.js @@ -27,9 +27,6 @@ import $ from 'jquery'; import 'plugins/kibana/discover/index'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -const SORTABLE_FIELDS = ['bytes', '@timestamp']; -const UNSORTABLE_FIELDS = ['request_body']; - describe('Doc Table', function () { let $parentScope; let $scope; @@ -119,155 +116,6 @@ describe('Doc Table', function () { }); }; - describe('kbnTableHeader', function () { - const $elem = angular.element(` - - `); - - beforeEach(function () { - init($elem, { - columns: [], - sortOrder: [], - onChangeSortOrder: sinon.stub(), - moveColumn: sinon.spy(), - removeColumn: sinon.spy(), - }); - }); - - afterEach(function () { - destroy(); - }); - - describe('adding and removing columns', function () { - columnTests('[data-test-subj~="docTableHeaderField"]', $elem); - }); - - describe('sorting button', function () { - beforeEach(function () { - $parentScope.columns = ['bytes', '_source']; - $elem.scope().$digest(); - }); - - it('should show for sortable columns', function () { - expect($elem.find(`[data-test-subj="docTableHeaderFieldSort_bytes"]`).length).to.be(1); - }); - - it('should not be shown for unsortable columns', function () { - expect($elem.find(`[data-test-subj="docTableHeaderFieldSort__source"]`).length).to.be(0); - }); - }); - - describe('cycleSortOrder function', function () { - it('should exist', function () { - expect($scope.cycleSortOrder).to.be.a(Function); - }); - - it('should call onChangeSortOrder with ascending order for a sortable field without sort order', function () { - $scope.sortOrder = []; - $scope.cycleSortOrder(SORTABLE_FIELDS[0]); - expect($scope.onChangeSortOrder.callCount).to.be(1); - expect($scope.onChangeSortOrder.firstCall.args).to.eql([SORTABLE_FIELDS[0], 'asc']); - }); - - it('should call onChangeSortOrder with ascending order for a sortable field already sorted by in descending order', function () { - $scope.sortOrder = [SORTABLE_FIELDS[0], 'desc']; - $scope.cycleSortOrder(SORTABLE_FIELDS[0]); - expect($scope.onChangeSortOrder.callCount).to.be(1); - expect($scope.onChangeSortOrder.firstCall.args).to.eql([SORTABLE_FIELDS[0], 'asc']); - }); - - it('should call onChangeSortOrder with ascending order for a sortable field when already sorted by an different field', function () { - $scope.sortOrder = [SORTABLE_FIELDS[1], 'asc']; - $scope.cycleSortOrder(SORTABLE_FIELDS[0]); - expect($scope.onChangeSortOrder.callCount).to.be(1); - expect($scope.onChangeSortOrder.firstCall.args).to.eql([SORTABLE_FIELDS[0], 'asc']); - }); - - it('should call onChangeSortOrder with descending order for a sortable field already sorted by in ascending order', function () { - $scope.sortOrder = [SORTABLE_FIELDS[0], 'asc']; - $scope.cycleSortOrder(SORTABLE_FIELDS[0]); - expect($scope.onChangeSortOrder.callCount).to.be(1); - expect($scope.onChangeSortOrder.firstCall.args).to.eql([SORTABLE_FIELDS[0], 'desc']); - }); - - it('should not call onChangeSortOrder for an unsortable field', function () { - $scope.sortOrder = []; - $scope.cycleSortOrder(UNSORTABLE_FIELDS[0]); - expect($scope.onChangeSortOrder.callCount).to.be(0); - }); - - it('should not try to call onChangeSortOrder when it is not defined', function () { - $scope.onChangeSortOrder = undefined; - expect(() => $scope.cycleSortOrder(SORTABLE_FIELDS[0])).to.not.throwException(); - }); - }); - - describe('headerClass function', function () { - it('should exist', function () { - expect($scope.headerClass).to.be.a(Function); - }); - - it('should return list including kbnDocTableHeader__sortChange for a sortable field not currently sorted by', function () { - expect($scope.headerClass(SORTABLE_FIELDS[0])).to.contain('kbnDocTableHeader__sortChange'); - }); - - it('should return undefined for an unsortable field', function () { - expect($scope.headerClass(UNSORTABLE_FIELDS[0])).to.be(undefined); - }); - - it('should return list including fa-sort-up for a sortable field not currently sorted by', function () { - expect($scope.headerClass(SORTABLE_FIELDS[0])).to.contain('fa-sort-up'); - }); - - it('should return list including fa-sort-up for a sortable field currently sorted by in ascending order', function () { - $scope.sortOrder = [SORTABLE_FIELDS[0], 'asc']; - expect($scope.headerClass(SORTABLE_FIELDS[0])).to.contain('fa-sort-up'); - }); - - it('should return list including fa-sort-down for a sortable field currently sorted by in descending order', function () { - $scope.sortOrder = [SORTABLE_FIELDS[0], 'desc']; - expect($scope.headerClass(SORTABLE_FIELDS[0])).to.contain('fa-sort-down'); - }); - }); - - describe('moving columns', function () { - beforeEach(function () { - $parentScope.columns = ['bytes', 'request_body', '@timestamp', 'point']; - $elem.scope().$digest(); - }); - - it('should move columns to the right', function () { - $scope.moveColumnRight('bytes'); - expect($scope.onMoveColumn.callCount).to.be(1); - expect($scope.onMoveColumn.firstCall.args).to.eql(['bytes', 1]); - }); - - it('shouldnt move the last column to the right', function () { - $scope.moveColumnRight('point'); - expect($scope.onMoveColumn.callCount).to.be(0); - }); - - it('should move columns to the left', function () { - $scope.moveColumnLeft('@timestamp'); - expect($scope.onMoveColumn.callCount).to.be(1); - expect($scope.onMoveColumn.firstCall.args).to.eql(['@timestamp', 1]); - }); - - it('shouldnt move the first column to the left', function () { - $scope.moveColumnLeft('bytes'); - expect($scope.onMoveColumn.callCount).to.be(0); - }); - }); - }); - describe('kbnTableRow', function () { const $elem = angular.element( ' - - - - - - - - - - {{getShortDotsName(name)}} - - - - - - - diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.js index 60d440b1f957..3af22a363e6d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header.js @@ -16,133 +16,19 @@ * specific language governing permissions and limitations * under the License. */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { shortenDottedString } from '../../../../common/utils/shorten_dotted_string'; -import headerHtml from './table_header.html'; +import { wrapInI18nContext } from 'ui/i18n'; import { uiModules } from 'ui/modules'; +import { TableHeader } from './table_header/table_header'; const module = uiModules.get('app/discover'); - -module.directive('kbnTableHeader', function () { - return { - restrict: 'A', - scope: { - columns: '=', - sortOrder: '=', - indexPattern: '=', - onChangeSortOrder: '=?', - onRemoveColumn: '=?', - onMoveColumn: '=?', - }, - template: headerHtml, - controller: function ($scope, config) { - $scope.hideTimeColumn = config.get('doc_table:hideTimeColumn'); - $scope.isShortDots = config.get('shortDots:enable'); - - $scope.getShortDotsName = function getShortDotsName(columnName) { - return $scope.isShortDots ? shortenDottedString(columnName) : columnName; - }; - - $scope.isSortableColumn = function isSortableColumn(columnName) { - return ( - !!$scope.indexPattern - && _.isFunction($scope.onChangeSortOrder) - && _.get($scope, ['indexPattern', 'fields', 'byName', columnName, 'sortable'], false) - ); - }; - - $scope.tooltip = function (column) { - if (!$scope.isSortableColumn(column)) return ''; - const name = $scope.isShortDots ? shortenDottedString(column) : column; - return i18n.translate('kbn.docTable.tableHeader.sortByColumnTooltip', { - defaultMessage: 'Sort by {columnName}', - values: { columnName: name }, - }); - }; - - $scope.canMoveColumnLeft = function canMoveColumn(columnName) { - return ( - _.isFunction($scope.onMoveColumn) - && $scope.columns.indexOf(columnName) > 0 - ); - }; - - $scope.canMoveColumnRight = function canMoveColumn(columnName) { - return ( - _.isFunction($scope.onMoveColumn) - && $scope.columns.indexOf(columnName) < $scope.columns.length - 1 - ); - }; - - $scope.canRemoveColumn = function canRemoveColumn(columnName) { - return ( - _.isFunction($scope.onRemoveColumn) - && (columnName !== '_source' || $scope.columns.length > 1) - ); - }; - - $scope.headerClass = function (column) { - if (!$scope.isSortableColumn(column)) return; - - const sortOrder = $scope.sortOrder; - const defaultClass = ['fa', 'fa-sort-up', 'kbnDocTableHeader__sortChange']; - - if (!sortOrder || column !== sortOrder[0]) return defaultClass; - return ['fa', sortOrder[1] === 'asc' ? 'fa-sort-up' : 'fa-sort-down']; - }; - - $scope.moveColumnLeft = function moveLeft(columnName) { - const newIndex = $scope.columns.indexOf(columnName) - 1; - - if (newIndex < 0) { - return; - } - - $scope.onMoveColumn(columnName, newIndex); - }; - - $scope.moveColumnRight = function moveRight(columnName) { - const newIndex = $scope.columns.indexOf(columnName) + 1; - - if (newIndex >= $scope.columns.length) { - return; - } - - $scope.onMoveColumn(columnName, newIndex); - }; - - $scope.cycleSortOrder = function cycleSortOrder(columnName) { - if (!$scope.isSortableColumn(columnName)) { - return; - } - - const [currentColumnName, currentDirection = 'asc'] = $scope.sortOrder; - const newDirection = ( - (columnName === currentColumnName && currentDirection === 'asc') - ? 'desc' - : 'asc' - ); - - $scope.onChangeSortOrder(columnName, newDirection); - }; - - $scope.getAriaLabelForColumn = function getAriaLabelForColumn(name) { - if (!$scope.isSortableColumn(name)) return null; - - const [currentColumnName, currentDirection = 'asc'] = $scope.sortOrder; - if(name === currentColumnName && currentDirection === 'asc') { - return i18n.translate('kbn.docTable.tableHeader.sortByColumnDescendingAriaLabel', { - defaultMessage: 'Sort {columnName} descending', - values: { columnName: name }, - }); - } - return i18n.translate('kbn.docTable.tableHeader.sortByColumnAscendingAriaLabel', { - defaultMessage: 'Sort {columnName} ascending', - values: { columnName: name }, - }); - }; +module.directive('kbnTableHeader', function (reactDirective, config) { + return reactDirective( + wrapInI18nContext(TableHeader), + undefined, + { restrict: 'A' }, + { + hideTimeColumn: config.get('doc_table:hideTimeColumn'), + isShortDots: config.get('shortDots:enable'), } - }; + ); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap new file mode 100644 index 000000000000..3860a03d5271 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap @@ -0,0 +1,221 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TableHeader with time column renders correctly 1`] = ` + + + + + Time + +
    diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.js b/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.js index cfc45e4af525..060658b11400 100644 --- a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.js +++ b/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.js @@ -21,7 +21,7 @@ import $ from 'jquery'; import { uiModules } from 'ui/modules'; import { DocViewsRegistryProvider } from 'ui/registry/doc_views'; -import 'ui/render_directive'; +import 'ui/directives/render_directive'; uiModules.get('apps/discover') .directive('docViewer', function (Private) { diff --git a/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/introduction.test.js.snap b/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/introduction.test.js.snap index f30ed0d1fac7..bc64579a4d71 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/introduction.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/introduction.test.js.snap @@ -13,10 +13,10 @@ exports[`props exportedFieldsUrl 1`] = ` -

    +

    Great tutorial   -

    +
    @@ -70,10 +70,10 @@ exports[`props iconType 1`] = ` -

    +

    Great tutorial   -

    +
    @@ -100,13 +100,13 @@ exports[`props isBeta 1`] = ` -

    +

    Great tutorial   -

    +
    @@ -133,10 +133,10 @@ exports[`props previewUrl 1`] = ` -

    +

    Great tutorial   -

    +
    @@ -172,10 +172,10 @@ exports[`render 1`] = ` -

    +

    Great tutorial   -

    +
    diff --git a/src/legacy/core_plugins/kibana/public/home/components/tutorial/introduction.js b/src/legacy/core_plugins/kibana/public/home/components/tutorial/introduction.js index 60cf56a51698..3ba2024d6b8c 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/tutorial/introduction.js +++ b/src/legacy/core_plugins/kibana/public/home/components/tutorial/introduction.js @@ -96,10 +96,10 @@ function IntroductionUI({ description, previewUrl, title, exportedFieldsUrl, ico {icon} -

    +

    {title}   {betaBadge} -

    +
    diff --git a/src/legacy/core_plugins/kibana/public/home/sample_data_client.js b/src/legacy/core_plugins/kibana/public/home/sample_data_client.js index 943eedb290f8..da46b3e16c09 100644 --- a/src/legacy/core_plugins/kibana/public/home/sample_data_client.js +++ b/src/legacy/core_plugins/kibana/public/home/sample_data_client.js @@ -24,7 +24,7 @@ import { indexPatternService } from './kibana_services'; const sampleDataUrl = '/api/sample_data'; function clearIndexPatternsCache() { - indexPatternService.getIds.clearCache(); + indexPatternService.clearCache(); } export async function listSampleDataSets() { diff --git a/src/legacy/core_plugins/kibana/public/management/_hacks.scss b/src/legacy/core_plugins/kibana/public/management/_hacks.scss index b2ffca9ef964..59af9c9617a3 100644 --- a/src/legacy/core_plugins/kibana/public/management/_hacks.scss +++ b/src/legacy/core_plugins/kibana/public/management/_hacks.scss @@ -23,21 +23,6 @@ kbn-management-objects-view { .ace_editor { height: 300px; } } -// SASSTODO: These are some dragula settings. -.gu-handle { - cursor: move; - cursor: grab; - cursor: -moz-grab; - cursor: -webkit-grab; -} - -.gu-mirror, -.gu-mirror .gu-handle { - cursor: grabbing; - cursor: -moz-grabbing; - cursor: -webkit-grabbing; -} - // Hack because the management wrapper is flat HTML and needs a class .mgtPage__body { max-width: map-get($euiBreakpoints, 'xl'); diff --git a/src/legacy/core_plugins/kibana/public/management/route_setup/load_default.js b/src/legacy/core_plugins/kibana/public/management/route_setup/load_default.js index 4b4957a71245..f797acbe8888 100644 --- a/src/legacy/core_plugins/kibana/public/management/route_setup/load_default.js +++ b/src/legacy/core_plugins/kibana/public/management/route_setup/load_default.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import React from 'react'; import { banners } from 'ui/notify'; -import { NoDefaultIndexPattern } from 'ui/index_patterns/errors'; +import { NoDefaultIndexPattern } from 'ui/index_patterns'; import uiRoutes from 'ui/routes'; import { EuiCallOut, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__jest__/create_index_pattern_wizard.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__jest__/create_index_pattern_wizard.test.js index 8ddb89797361..20e2fb779abe 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__jest__/create_index_pattern_wizard.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__jest__/create_index_pattern_wizard.test.js @@ -55,6 +55,7 @@ const services = { config: {}, changeUrl: () => {}, scopeApply: () => {}, + indexPatternCreationType: mockIndexPatternCreationType, }; @@ -183,7 +184,7 @@ describe('CreateIndexPatternWizard', () => { get: () => ({ create, }), - cache: { clear } + clearCache: clear, }, changeUrl, indexPatternCreationType: mockIndexPatternCreationType diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/__jest__/__snapshots__/step_time_field.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/__jest__/__snapshots__/step_time_field.test.js.snap index 7b442d569ae8..3f25b180c9e1 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/__jest__/__snapshots__/step_time_field.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/__jest__/__snapshots__/step_time_field.test.js.snap @@ -31,7 +31,7 @@ exports[`StepTimeField should render "Custom index pattern ID already exists" wh /> ({ const mockIndexPatternCreationType = { getIndexPatternType: () => 'default', - getIndexPatternName: () => 'name' + getIndexPatternName: () => 'name', + getFetchForWildcardOptions: () => {} }; const noop = () => {}; const indexPatternsService = { - fieldsFetcher: { - fetchForWildcard: noop, - } + make: async () => ({ + fieldsFetcher: { + fetch: noop + } + }) }; describe('StepTimeField', () => { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.js index d500924376df..4476ad868cbc 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/step_time_field.js @@ -81,12 +81,15 @@ export class StepTimeFieldComponent extends Component { } fetchTimeFields = async () => { - const { indexPatternsService, indexPattern } = this.props; + const { indexPatternsService, indexPattern: pattern } = this.props; const { getFetchForWildcardOptions } = this.props.indexPatternCreationType; + const indexPattern = await indexPatternsService.make(); + indexPattern.title = pattern; + this.setState({ isFetchingTimeFields: true }); const fields = await ensureMinimumTime( - indexPatternsService.fieldsFetcher.fetchForWildcard(indexPattern, getFetchForWildcardOptions()) + indexPattern.fieldsFetcher.fetch(getFetchForWildcardOptions()) ); const timeFields = extractTimeFields(fields); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js index 5bf791d4a662..021b42c9f59f 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js @@ -76,7 +76,7 @@ export class CreateIndexPatternWizard extends Component { this.setState(prevState => ({ toasts: prevState.toasts.concat([{ title: errorMsg, - id: errorMsg, + id: errorMsg.props.id, color: 'warning', iconType: 'alert', }]) @@ -146,7 +146,7 @@ export class CreateIndexPatternWizard extends Component { await services.config.set('defaultIndex', createdId); } - services.indexPatterns.cache.clear(createdId); + services.indexPatterns.clearCache(createdId); services.changeUrl(`/management/kibana/index_patterns/${createdId}`); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js index ebbbad83ae7d..b72a28d6f721 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js @@ -17,7 +17,7 @@ * under the License. */ -import { Field } from 'ui/index_patterns/_field'; +import { Field } from 'ui/index_patterns'; import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; import { docTitle } from 'ui/doc_title'; import { KbnUrlProvider } from 'ui/url'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js index a67c3ed99550..1eb56403d3a7 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js @@ -100,8 +100,9 @@ function updateScriptedFieldsTable($scope, $state) { getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route), }} onRemoveField={() => { - $scope.editSections = $scope.editSectionsProvider($scope.indexPattern, $scope.indexPatternListProvider); + $scope.editSections = $scope.editSectionsProvider($scope.indexPattern, $scope.fieldFilter, $scope.indexPatternListProvider); $scope.refreshFilters(); + $scope.$apply(); }} /> , @@ -263,7 +264,7 @@ uiModules.get('apps/management') } } - Promise.resolve(indexPatterns.delete($scope.indexPattern)) + Promise.resolve($scope.indexPattern.destroy()) .then(function () { $location.url('/management/kibana/index_patterns'); }) diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap index 9dbfc9866536..f9b79266d425 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap @@ -209,9 +209,7 @@ exports[`ObjectsTable import should show the flyout 1`] = ` done={[Function]} indexPatterns={ Object { - "cache": Object { - "clearAll": [MockFunction], - }, + "clearCache": [MockFunction], } } newIndexPatternUrl="" diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js index 236970cf3c3f..51f33f756836 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js @@ -123,9 +123,7 @@ const defaultProps = { bulkGet: jest.fn(), }, indexPatterns: { - cache: { - clearAll: jest.fn(), - } + clearCache: jest.fn(), }, $http, basePath: '', @@ -531,7 +529,7 @@ describe('ObjectsTable', () => { await component.instance().delete(); - expect(defaultProps.indexPatterns.cache.clearAll).toHaveBeenCalled(); + expect(defaultProps.indexPatterns.clearCache).toHaveBeenCalled(); expect(mockSavedObjectsClient.bulkGet).toHaveBeenCalledWith(mockSelectedSavedObjects); expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith(mockSavedObjects[0].type, mockSavedObjects[0].id); expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith(mockSavedObjects[1].type, mockSavedObjects[1].id); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap index 015683cdde67..0586647254e9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap @@ -611,6 +611,7 @@ exports[`Flyout should render import step 1`] = ` > { const options = this.state.indexPatterns.map(indexPattern => ({ - text: indexPattern.get('title'), + text: indexPattern.title, value: indexPattern.id, - ['data-test-subj']: `indexPatternOption-${indexPattern.get('title')}`, + ['data-test-subj']: `indexPatternOption-${indexPattern.title}`, })); options.unshift({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js index 9c1f1a84e1bb..263b23859c8a 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js @@ -369,7 +369,7 @@ class ObjectsTableUI extends Component { object => object.type === 'index-pattern' ); if (indexPatterns.length) { - await this.props.indexPatterns.cache.clearAll(); + await this.props.indexPatterns.clearCache(); } const objects = await savedObjectsClient.bulkGet(selectedSavedObjects); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js index cb6e81c71493..1468b3d90400 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js @@ -96,7 +96,7 @@ async function importIndexPattern(doc, indexPatterns, overwriteAll, confirmModal return; } } - indexPatterns.cache.clear(newId); + indexPatterns.clearCache(newId); return newId; } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap index b0e3503b73f2..e7f66a14cc82 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap @@ -899,6 +899,7 @@ exports[`Field for image setting should render as read only if saving is disable compressed={false} data-test-subj="advancedSetting-editField-image:test:setting" disabled={true} + display="large" initialPromptText="Select or drag and drop a file" onChange={[Function]} onKeyDown={[Function]} @@ -1069,6 +1070,7 @@ exports[`Field for image setting should render custom setting icon if it is cust compressed={false} data-test-subj="advancedSetting-editField-image:test:setting" disabled={false} + display="large" initialPromptText="Select or drag and drop a file" onChange={[Function]} onKeyDown={[Function]} @@ -1133,6 +1135,7 @@ exports[`Field for image setting should render default value if there is no user compressed={false} data-test-subj="advancedSetting-editField-image:test:setting" disabled={false} + display="large" initialPromptText="Select or drag and drop a file" onChange={[Function]} onKeyDown={[Function]} diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index 6db6e2c9ad1c..4c98e54168c7 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -402,23 +402,25 @@ function VisEditor( $scope.$listenAndDigestAsync(timefilter, 'timeUpdate', updateTimeRange); $scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', updateRefreshInterval); - // update the searchSource when filters update - const filterUpdateSubscription = subscribeWithScope($scope, queryFilter.getUpdates$(), { - next: () => { - $scope.filters = queryFilter.getFilters(); - $scope.fetch(); - } - }); - // update the searchSource when query updates $scope.fetch = function () { $state.save(); savedVis.searchSource.setField('query', $state.query); savedVis.searchSource.setField('filter', $state.filters); - $scope.globalFilters = queryFilter.getGlobalFilters(); $scope.vis.forceReload(); }; + // update the searchSource when filters update + const filterUpdateSubscription = subscribeWithScope($scope, queryFilter.getUpdates$(), { + next: () => { + $scope.filters = queryFilter.getFilters(); + $scope.globalFilters = queryFilter.getGlobalFilters(); + } + }); + const filterFetchSubscription = subscribeWithScope($scope, queryFilter.getFetches$(), { + next: $scope.fetch + }); + $scope.$on('$destroy', function () { if ($scope._handler) { $scope._handler.destroy(); @@ -426,6 +428,7 @@ function VisEditor( savedVis.destroy(); stateMonitor.destroy(); filterUpdateSubscription.unsubscribe(); + filterFetchSubscription.unsubscribe(); }); if (!$scope.chrome.getVisible()) { @@ -441,9 +444,16 @@ function VisEditor( } $scope.updateQueryAndFetch = function ({ query, dateRange }) { - timefilter.setTime(dateRange); + const isUpdate = ( + (query && !_.isEqual(query, $state.query)) || + (dateRange && !_.isEqual(dateRange, $scope.timeRange)) + ); + $state.query = query; - $scope.fetch(); + timefilter.setTime(dateRange); + + // If nothing has changed, trigger the fetch manually, otherwise it will happen as a result of the changes + if (!isUpdate) $scope.fetch(); }; $scope.onRefreshChange = function ({ isPaused, refreshInterval }) { diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/get_index_pattern.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/get_index_pattern.ts index 820be92d6829..699fa68b4528 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/get_index_pattern.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/get_index_pattern.ts @@ -18,8 +18,7 @@ */ import chrome from 'ui/chrome'; -import { StaticIndexPattern } from 'ui/index_patterns'; -import { getFromSavedObject } from 'ui/index_patterns/static_utils'; +import { StaticIndexPattern, getFromSavedObject } from 'ui/index_patterns'; import { VisSavedObject } from 'ui/visualize/loader/types'; export async function getIndexPattern( diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index a59029fc36ef..c1b9bfd42cc4 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -38,8 +38,8 @@ import { import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { TimeRange } from 'ui/timefilter/time_history'; -import { Query } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; +import { Query, onlyDisabledFiltersChanged } from '../../../../data/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -76,6 +76,7 @@ export class VisualizeEmbeddable extends Embeddable { - this.reload(); this.handleChanges(); }); } @@ -138,14 +138,17 @@ export class VisualizeEmbeddable extends Embeddable { - this.uiState.set(key, visCustomizations[key]); - }); - this.uiState.on('change', this.uiStateChangeHandler); + if (!_.isEqual(visCustomizations, this.visCustomizations)) { + this.visCustomizations = visCustomizations; + // Turn this off or the uiStateChangeHandler will fire for every modification. + this.uiState.off('change', this.uiStateChangeHandler); + this.uiState.clearAllKeys(); + this.uiState.set('vis', visCustomizations); + getKeys(visCustomizations).forEach(key => { + this.uiState.set(key, visCustomizations[key]); + }); + this.uiState.on('change', this.uiStateChangeHandler); + } } else { this.uiState.clearAllKeys(); } @@ -157,19 +160,19 @@ export class VisualizeEmbeddable extends Embeddable { public readonly type = VISUALIZE_EMBEDDABLE_TYPE; + private readonly visTypes: VisTypesRegistry; static async createVisualizeEmbeddableFactory(): Promise { const $injector = await chrome.dangerouslyGetActiveInjector(); @@ -88,32 +89,31 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< return new VisualizeEmbeddableFactory(visTypes); } - constructor(private visTypes: VisTypesRegistry) { + constructor(visTypes: VisTypesRegistry) { super({ savedObjectMetaData: { name: i18n.translate('kbn.visualize.savedObjectName', { defaultMessage: 'Visualization' }), type: 'visualization', getIconForSavedObject: savedObject => { - if (!this.visTypes) { + if (!visTypes) { return 'visualizeApp'; } return ( - this.visTypes.byName[JSON.parse(savedObject.attributes.visState).type].icon || - 'visualizeApp' + visTypes.byName[JSON.parse(savedObject.attributes.visState).type].icon || 'visualizeApp' ); }, getTooltipForSavedObject: savedObject => { - if (!this.visTypes) { + if (!visTypes) { return ''; } - return `${savedObject.attributes.title} (${this.visTypes.byName[JSON.parse(savedObject.attributes.visState).type].title})`; + return `${savedObject.attributes.title} (${visTypes.byName[JSON.parse(savedObject.attributes.visState).type].title})`; }, showSavedObject: savedObject => { - if (!this.visTypes) { + if (!visTypes) { return false; } const typeName: string = JSON.parse(savedObject.attributes.visState).type; - const visType = this.visTypes.byName[typeName]; + const visType = visTypes.byName[typeName]; if (!visType) { return false; } @@ -124,6 +124,8 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< }, }, }); + + this.visTypes = visTypes; } public isEditable() { diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.js b/src/legacy/core_plugins/kibana/public/visualize/index.js index 9548a05a355f..b3c16fb94d7f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/index.js +++ b/src/legacy/core_plugins/kibana/public/visualize/index.js @@ -29,8 +29,8 @@ import { VisualizeConstants } from './visualize_constants'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; import { getLandingBreadcrumbs, getWizardStep1Breadcrumbs } from './breadcrumbs'; -import { data } from 'plugins/data/setup'; -data.filter.loadLegacyDirectives(); +// load directives +import '../../../data/public'; uiRoutes .defaults(/visualize/, { diff --git a/src/legacy/core_plugins/kibana/server/routes/api/export/index.js b/src/legacy/core_plugins/kibana/server/routes/api/export/index.js index a2469d8f5c5a..d47aa4b52a0b 100644 --- a/src/legacy/core_plugins/kibana/server/routes/api/export/index.js +++ b/src/legacy/core_plugins/kibana/server/routes/api/export/index.js @@ -17,10 +17,11 @@ * under the License. */ -import { exportDashboards } from '../../../lib/export/export_dashboards'; -import Boom from 'boom'; import Joi from 'joi'; import moment from 'moment'; + +import { exportDashboards } from '../../../lib/export/export_dashboards'; + export function exportApi(server) { server.route({ path: '/api/kibana/dashboards/export', @@ -46,8 +47,7 @@ export function exportApi(server) { .header('Content-Disposition', `attachment; filename="${filename}"`) .header('Content-Type', 'application/json') .header('Content-Length', Buffer.byteLength(json, 'utf8')); - }) - .catch(err => Boom.boomify(err, { statusCode: 400 })); + }); } }); } diff --git a/src/legacy/core_plugins/kibana/server/routes/api/import/index.js b/src/legacy/core_plugins/kibana/server/routes/api/import/index.js index c7291798c6d6..7926e8775eee 100644 --- a/src/legacy/core_plugins/kibana/server/routes/api/import/index.js +++ b/src/legacy/core_plugins/kibana/server/routes/api/import/index.js @@ -17,7 +17,6 @@ * under the License. */ -import Boom from 'boom'; import Joi from 'joi'; import { importDashboards } from '../../../lib/import/import_dashboards'; @@ -40,11 +39,7 @@ export function importApi(server) { }, handler: async (req) => { - try { - return await importDashboards(req); - } catch (err) { - throw Boom.boomify(err, { statusCode: 400 }); - } + return await importDashboards(req); } }); } diff --git a/src/legacy/core_plugins/kibana/server/tutorials/apm/instructions/apm_agent_instructions.js b/src/legacy/core_plugins/kibana/server/tutorials/apm/instructions/apm_agent_instructions.js index c2186b12a7b0..0f375ff06077 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/apm/instructions/apm_agent_instructions.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/apm/instructions/apm_agent_instructions.js @@ -472,17 +472,16 @@ export const createDotNetAgentInstructions = (apmServerUrl = '', secretToken = ' defaultMessage: 'Download the APM agent', }), textPre: i18n.translate('kbn.server.tutorials.apm.dotNetClient.download.textPre', { - defaultMessage: '**Warning: The .NET agent is currently in Beta and not meant for production use.** \n\n \ - Add the the agent package(s) from [NuGet]({allNuGetPacakgesLink}) to your .NET application. There are multiple \ - NuGet packages available for different use cases. \n\n For an ASP.NET Core application with Entity Framework \ - Core download the [Elastic.Apm.All]({allApmPackageLink}) package. This package will automatically add every agent component to \ - your application. \n\n In case you would like to to minimize the dependencies, you can use the \ + defaultMessage: 'Add the the agent package(s) from [NuGet]({allNuGetPackagesLink}) to your .NET application. There are multiple \ + NuGet packages available for different use cases. \n\nFor an ASP.NET Core application with Entity Framework \ + Core download the [Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}) package. This package will automatically add every \ + agent component to your application. \n\n In case you would like to to minimize the dependencies, you can use the \ [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) package for just \ ASP.NET Core monitoring or the [Elastic.Apm.EfCore]({efCorePackageLink}) package for just Entity Framework Core monitoring. \n\n \ In case you only want to use the public Agent API for manual instrumentation use the [Elastic.Apm]({elasticApmPackageLink}) package.', values: { - allNuGetPacakgesLink: 'https://www.nuget.org/packages?q=Elastic.apm', - allApmPackageLink: 'https://www.nuget.org/packages/Elastic.Apm.All', + allNuGetPackagesLink: 'https://www.nuget.org/packages?q=Elastic.apm', + netCoreAllApmPackageLink: 'https://www.nuget.org/packages/Elastic.Apm.NetCoreAll', aspNetCorePackageLink: 'https://www.nuget.org/packages/Elastic.Apm.AspNetCore', efCorePackageLink: 'https://www.nuget.org/packages/Elastic.Apm.EntityFrameworkCore', elasticApmPackageLink: 'https://www.nuget.org/packages/Elastic.Apm', @@ -494,13 +493,14 @@ export const createDotNetAgentInstructions = (apmServerUrl = '', secretToken = ' defaultMessage: 'Add the agent to the application', }), textPre: i18n.translate('kbn.server.tutorials.apm.dotNetClient.configureApplication.textPre', { - defaultMessage: 'In case of ASP.NET Core, call the `UseElasticApm` method in the `Configure` method within the `Startup.cs` file.' + defaultMessage: 'In case of ASP.NET Core with the `Elastic.Apm.NetCoreAll` package, call the `UseAllElasticApm` \ + method in the `Configure` method within the `Startup.cs` file.' }), commands: `public class Startup {curlyOpen} public void Configure(IApplicationBuilder app, IHostingEnvironment env) {curlyOpen} - app.UseElasticApm(Configuration); + app.UseAllElasticApm(Configuration); //…rest of the method {curlyClose} //…rest of the class @@ -515,8 +515,7 @@ export const createDotNetAgentInstructions = (apmServerUrl = '', secretToken = ' defaultMessage: 'Sample appsettings.json file:', }), commands: `{curlyOpen} - "ElasticApm": {curlyOpen} - "LogLevel": "Error", + "ElasticApm": {curlyOpen} "SecretToken": "${secretToken}", "ServerUrls": "${apmServerUrl || 'http://localhost:8200'}", //Set custom APM Server URL (default: http://localhost:8200) "ServiceName" : "MyApp", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index dc67b42bafc5..088a2e9d8178 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -298,6 +298,19 @@ export function getUiSettingDefaults() { }), category: ['discover'], }, + 'discover:searchOnPageLoad': { + name: i18n.translate('kbn.advancedSettings.discover.searchOnPageLoadTitle', { + defaultMessage: 'Search on page load', + }), + value: true, + type: 'boolean', + description: i18n.translate('kbn.advancedSettings.discover.searchOnPageLoadText', { + defaultMessage: + 'Controls whether a search is executed when Discover first loads. This setting does not ' + + 'have an effect when loading a saved search.', + }), + category: ['discover'], + }, 'doc_table:highlight': { name: i18n.translate('kbn.advancedSettings.docTableHighlightTitle', { defaultMessage: 'Highlight results', diff --git a/src/legacy/core_plugins/kibana_react/public/index.ts b/src/legacy/core_plugins/kibana_react/public/index.ts index 3c82192bda54..980fbe2e4616 100644 --- a/src/legacy/core_plugins/kibana_react/public/index.ts +++ b/src/legacy/core_plugins/kibana_react/public/index.ts @@ -24,4 +24,3 @@ /** @public types */ export { TopNavMenu, TopNavMenuData } from './top_nav_menu'; -export { SearchBar, SearchBarProps } from './search_bar'; diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx index 03213afa8a5d..764e93a8685e 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx @@ -22,7 +22,7 @@ import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -jest.mock('../search_bar', () => { +jest.mock('../../../../core_plugins/data/public', () => { return { SearchBar: () =>
    , SearchBarProps: {}, diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx index 31ad676bb4cb..e0c705ece7b4 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx +++ b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx @@ -23,7 +23,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { SearchBar, SearchBarProps } from '../search_bar'; +import { SearchBar, SearchBarProps } from '../../../../core_plugins/data/public'; type Props = Partial & { name: string; diff --git a/src/legacy/core_plugins/metric_vis/index.js b/src/legacy/core_plugins/metric_vis/index.js deleted file mode 100644 index 3738baccb5c5..000000000000 --- a/src/legacy/core_plugins/metric_vis/index.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 { resolve } from 'path'; - -export default function (kibana) { - - return new kibana.Plugin({ - - uiExports: { - visTypes: [ - 'plugins/metric_vis/metric_vis' - ], - interpreter: ['plugins/metric_vis/metric_vis_fn'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - } - - }); - -} diff --git a/src/legacy/core_plugins/metric_vis/public/__tests__/metric_vis.js b/src/legacy/core_plugins/metric_vis/public/__tests__/metric_vis.js deleted file mode 100644 index a8bfec6c8620..000000000000 --- a/src/legacy/core_plugins/metric_vis/public/__tests__/metric_vis.js +++ /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 $ from 'jquery'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; - -import { VisProvider } from 'ui/vis'; -import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern'; -import MetricVisProvider from '../metric_vis'; - -describe('metric vis', () => { - let setup = null; - let vis; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject((Private) => { - setup = () => { - const Vis = Private(VisProvider); - const metricVisType = Private(MetricVisProvider); - const indexPattern = Private(LogstashIndexPatternStubProvider); - - indexPattern.stubSetFieldFormat('ip', 'url', { - urlTemplate: 'http://ip.info?address={{value}}', - labelTemplate: 'ip[{{value}}]' - }); - - vis = new Vis(indexPattern, { - type: 'metric', - aggs: [{ id: '1', type: 'top_hits', schema: 'metric', params: { field: 'ip' } }], - }); - - vis.params.dimensions = { - metrics: [{ - accessor: 0, format: { - id: 'url', params: { - urlTemplate: 'http://ip.info?address={{value}}', - labelTemplate: 'ip[{{value}}]' - } - } - }] - }; - - const el = document.createElement('div'); - const Controller = metricVisType.visualization; - const controller = new Controller(el, vis); - const render = (esResponse) => { - controller.render(esResponse, vis.params); - }; - - return { el, render }; - }; - })); - - it('renders html value from field formatter', () => { - const { el, render } = setup(); - - const ip = '235.195.237.208'; - render({ - columns: [{ id: 'col-0', name: 'ip' }], - rows: [{ 'col-0': ip }] - }); - - const $link = $(el) - .find('a[href]') - .filter(function () { return this.href.includes('ip.info'); }); - - expect($link).to.have.length(1); - expect($link.text()).to.be(`ip[${ip}]`); - }); -}); diff --git a/src/legacy/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js b/src/legacy/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js deleted file mode 100644 index 82ef6f2de50b..000000000000 --- a/src/legacy/core_plugins/metric_vis/public/__tests__/metric_vis_controller.js +++ /dev/null @@ -1,73 +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 { MetricVisComponent } from '../metric_vis_controller'; - -describe('metric vis controller', function () { - - const vis = { - params: { - metric: { - colorSchema: 'Green to Red', - colorsRange: [ - { from: 0, to: 1000 } - ], - style: {}, - }, dimensions: { - metrics: [{ accessor: 0 }], - bucket: null, - } - } - }; - - let metricController; - - beforeEach(() => { - metricController = new MetricVisComponent({ vis: vis, visParams: vis.params }); - }); - - it('should set the metric label and value', function () { - const metrics = metricController._processTableGroups({ - columns: [{ id: 'col-0', name: 'Count' }], - rows: [{ 'col-0': 4301021 }] - }); - - expect(metrics.length).to.be(1); - expect(metrics[0].label).to.be('Count'); - expect(metrics[0].value).to.be(4301021); - }); - - it('should support multi-value metrics', function () { - vis.params.dimensions.metrics.push({ accessor: 1 }); - const metrics = metricController._processTableGroups({ - columns: [ - { id: 'col-0', name: '1st percentile of bytes' }, - { id: 'col-1', name: '99th percentile of bytes' } - ], - rows: [{ 'col-0': 182, 'col-1': 445842.4634666484 }] - }); - - expect(metrics.length).to.be(2); - expect(metrics[0].label).to.be('1st percentile of bytes'); - expect(metrics[0].value).to.be(182); - expect(metrics[1].label).to.be('99th percentile of bytes'); - expect(metrics[1].value).to.be(445842.4634666484); - }); -}); diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis.js b/src/legacy/core_plugins/metric_vis/public/metric_vis.js deleted file mode 100644 index 70ccab3af97d..000000000000 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis.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 './metric_vis_params'; -import { i18n } from '@kbn/i18n'; -import { VisFactoryProvider } from 'ui/vis/vis_factory'; -import { Schemas } from 'ui/vis/editors/default/schemas'; -import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; -import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; -import { MetricVisComponent } from './metric_vis_controller'; -// we need to load the css ourselves - -// we also need to load the controller and used by the template - -// register the provider with the visTypes registry -VisTypesRegistryProvider.register(MetricVisProvider); - -function MetricVisProvider(Private) { - const VisFactory = Private(VisFactoryProvider); - - // return the visType object, which kibana will use to display and configure new - // Vis object of this type. - return VisFactory.createReactVisualization({ - name: 'metric', - title: i18n.translate('metricVis.metricTitle', { defaultMessage: 'Metric' }), - icon: 'visMetric', - description: i18n.translate('metricVis.metricDescription', { defaultMessage: 'Display a calculation as a single number' }), - visConfig: { - component: MetricVisComponent, - defaults: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [ - { from: 0, to: 10000 } - ], - labels: { - show: true - }, - invertColors: false, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 60, - } - } - } - }, - editorConfig: { - collections: { - metricColorMode: [ - { - id: 'None', - label: i18n.translate('metricVis.colorModes.noneOptionLabel', { defaultMessage: 'None' }) - }, - { - id: 'Labels', - label: i18n.translate('metricVis.colorModes.labelsOptionLabel', { defaultMessage: 'Labels' }) - }, - { - id: 'Background', - label: i18n.translate('metricVis.colorModes.backgroundOptionLabel', { defaultMessage: 'Background' }) - } - ], - colorSchemas: Object.values(vislibColorMaps).map(value => ({ id: value.id, label: value.label })), - }, - optionsTemplate: '', - schemas: new Schemas([ - { - group: 'metrics', - name: 'metric', - title: i18n.translate('metricVis.schemas.metricTitle', { defaultMessage: 'Metric' }), - min: 1, - aggFilter: [ - '!std_dev', '!geo_centroid', - '!derivative', '!serial_diff', '!moving_avg', '!cumulative_sum', '!geo_bounds'], - aggSettings: { - top_hits: { - allowStrings: true - }, - }, - defaults: [ - { type: 'count', schema: 'metric' } - ] - }, { - group: 'buckets', - name: 'group', - title: i18n.translate('metricVis.schemas.splitGroupTitle', { defaultMessage: 'Split group' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - } - ]) - } - }); -} - -// export the provider so that the visType can be required with Private() -export default MetricVisProvider; diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js b/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js deleted file mode 100644 index ecbb9d917874..000000000000 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js +++ /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 _ from 'lodash'; -import React, { Component } from 'react'; -import { getHeatmapColors } from 'ui/vislib/components/color/heatmap_color'; -import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { isColorDark } from '@elastic/eui'; - -import { MetricVisValue } from './components/metric_vis_value'; - -export class MetricVisComponent extends Component { - - _getLabels() { - const config = this.props.visParams.metric; - const isPercentageMode = config.percentageMode; - const colorsRange = config.colorsRange; - const max = _.last(colorsRange).to; - const labels = []; - colorsRange.forEach(range => { - const from = isPercentageMode ? Math.round(100 * range.from / max) : range.from; - const to = isPercentageMode ? Math.round(100 * range.to / max) : range.to; - labels.push(`${from} - ${to}`); - }); - - return labels; - } - - _getColors() { - const config = this.props.visParams.metric; - const invertColors = config.invertColors; - const colorSchema = config.colorSchema; - const colorsRange = config.colorsRange; - const labels = this._getLabels(); - const colors = {}; - for (let i = 0; i < labels.length; i += 1) { - const divider = Math.max(colorsRange.length - 1, 1); - const val = invertColors ? 1 - i / divider : i / divider; - colors[labels[i]] = getHeatmapColors(val, colorSchema); - } - return colors; - } - - _getBucket(val) { - const config = this.props.visParams.metric; - let bucket = _.findIndex(config.colorsRange, range => { - return range.from <= val && range.to > val; - }); - - if (bucket === -1) { - if (val < config.colorsRange[0].from) bucket = 0; - else bucket = config.colorsRange.length - 1; - } - - return bucket; - } - - _getColor(val, labels, colors) { - const bucket = this._getBucket(val); - const label = labels[bucket]; - return colors[label]; - } - - _needsLightText(bgColor) { - const color = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(bgColor); - if (!color) { - return false; - } - return isColorDark(parseInt(color[1]), parseInt(color[2]), parseInt(color[3])); - } - - _getFormattedValue = (fieldFormatter, value, format = 'text') => { - if (_.isNaN(value)) return '-'; - return fieldFormatter.convert(value, format); - }; - - _processTableGroups(table) { - const config = this.props.visParams.metric; - const dimensions = this.props.visParams.dimensions; - const isPercentageMode = config.percentageMode; - const min = config.colorsRange[0].from; - const max = _.last(config.colorsRange).to; - const colors = this._getColors(); - const labels = this._getLabels(); - const metrics = []; - - let bucketColumnId; - let bucketFormatter; - - if (dimensions.bucket) { - bucketColumnId = table.columns[dimensions.bucket.accessor].id; - bucketFormatter = getFormat(dimensions.bucket.format); - } - - dimensions.metrics.forEach(metric => { - const columnIndex = metric.accessor; - const column = table.columns[columnIndex]; - const formatter = getFormat(metric.format); - table.rows.forEach((row, rowIndex) => { - - let title = column.name; - let value = row[column.id]; - const color = this._getColor(value, labels, colors); - - if (isPercentageMode) { - value = (value - min) / (max - min); - } - value = this._getFormattedValue(formatter, value, 'html'); - - if (bucketColumnId) { - const bucketValue = this._getFormattedValue(bucketFormatter, row[bucketColumnId]); - title = `${bucketValue} - ${title}`; - } - - const shouldColor = config.colorsRange.length > 1; - - metrics.push({ - label: title, - value: value, - color: shouldColor && config.style.labelColor ? color : null, - bgColor: shouldColor && config.style.bgColor ? color : null, - lightText: shouldColor && config.style.bgColor && this._needsLightText(color), - rowIndex: rowIndex, - }); - }); - }); - - return metrics; - } - - _filterBucket = (metric) => { - const dimensions = this.props.visParams.dimensions; - if (!dimensions.bucket) { - return; - } - const table = this.props.visData; - this.props.vis.API.events.filter({ table, column: dimensions.bucket.accessor, row: metric.rowIndex }); - }; - - _renderMetric = (metric, index) => { - return ( - - ); - }; - - render() { - let metricsHtml; - if (this.props.visData) { - const metrics = this._processTableGroups(this.props.visData); - metricsHtml = metrics.map(this._renderMetric); - } - return (
    {metricsHtml}
    ); - } - - componentDidMount() { - this.props.renderComplete(); - } - - componentDidUpdate() { - this.props.renderComplete(); - } -} diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.js b/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.js deleted file mode 100644 index 1ba28866d4e4..000000000000 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.js +++ /dev/null @@ -1,176 +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 { functionsRegistry } from 'plugins/interpreter/registries'; -import { i18n } from '@kbn/i18n'; -import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; - -export const metric = () => ({ - name: 'metricVis', - type: 'render', - context: { - types: [ - 'kibana_datatable' - ], - }, - help: i18n.translate('metricVis.function.help', { - defaultMessage: 'Metric visualization' - }), - args: { - percentage: { - types: ['boolean'], - default: false, - help: i18n.translate('metricVis.function.percentage.help', { - defaultMessage: 'Shows metric in percentage mode. Requires colorRange to be set.' - }) - }, - colorScheme: { - types: ['string'], - default: '"Green to Red"', - options: Object.values(vislibColorMaps).map(value => value.id), - help: i18n.translate('metricVis.function.colorScheme.help', { - defaultMessage: 'Color scheme to use' - }) - }, - colorMode: { - types: ['string'], - default: '"None"', - options: ['None', 'Label', 'Background'], - help: i18n.translate('metricVis.function.colorMode.help', { - defaultMessage: 'Which part of metric to color' - }) - }, - colorRange: { - types: ['range'], - multi: true, - help: i18n.translate('metricVis.function.colorRange.help', { - defaultMessage: 'A range object specifying groups of values to which different colors should be applied.' - }) - }, - useRanges: { - types: ['boolean'], - default: false, - help: i18n.translate('metricVis.function.useRanges.help', { - defaultMessage: 'Enabled color ranges.' - }) - }, - invertColors: { - types: ['boolean'], - default: false, - help: i18n.translate('metricVis.function.invertColors.help', { - defaultMessage: 'Inverts the color ranges' - }) - }, - showLabels: { - types: ['boolean'], - default: true, - help: i18n.translate('metricVis.function.showLabels.help', { - defaultMessage: 'Shows labels under the metric values.' - }) - }, - bgFill: { - types: ['string'], - default: '"#000"', - aliases: ['backgroundFill', 'bgColor', 'backgroundColor'], - help: i18n.translate('metricVis.function.bgFill.help', { - defaultMessage: 'Color as html hex code (#123456), html color (red, blue) or rgba value (rgba(255,255,255,1)).' - }) - }, - font: { - types: ['style'], - help: i18n.translate('metricVis.function.font.help', { - defaultMessage: 'Font settings.' - }), - default: '{font size=60}', - }, - subText: { - types: ['string'], - aliases: ['label', 'text', 'description'], - default: '""', - help: i18n.translate('metricVis.function.subText.help', { - defaultMessage: 'Custom text to show under the metric' - }) - }, - metric: { - types: ['vis_dimension'], - help: i18n.translate('metricVis.function.metric.help', { - defaultMessage: 'metric dimension configuration' - }), - required: true, - multi: true, - }, - bucket: { - types: ['vis_dimension'], - help: i18n.translate('metricVis.function.bucket.help', { - defaultMessage: 'bucket dimension configuration' - }), - }, - }, - fn(context, args) { - - const dimensions = { - metrics: args.metric, - }; - - if (args.bucket) { - dimensions.bucket = args.bucket; - } - - if (args.percentage && (!args.colorRange || args.colorRange.length === 0)) { - throw new Error ('colorRange must be provided when using percentage'); - } - - const fontSize = parseInt(args.font.spec.fontSize); - - return { - type: 'render', - as: 'visualization', - value: { - visData: context, - visType: 'metric', - visConfig: { - metric: { - percentageMode: args.percentage, - useRanges: args.useRanges, - colorSchema: args.colorScheme, - metricColorMode: args.colorMode, - colorsRange: args.colorRange, - labels: { - show: args.showLabels, - }, - invertColors: args.invertColors, - style: { - bgFill: args.bgFill, - bgColor: args.colorMode === 'Background', - labelColor: args.colorMode === 'Labels', - subText: args.subText, - fontSize, - } - }, - dimensions, - }, - params: { - listenOnChange: true, - } - }, - }; - }, -}); - -functionsRegistry.register(metric); diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.test.js b/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.test.js deleted file mode 100644 index f1705d644c9c..000000000000 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.test.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 { functionWrapper } from '../../interpreter/test_helpers'; -import { metric } from './metric_vis_fn'; - -jest.mock('ui/new_platform'); - -describe('interpreter/functions#metric', () => { - const fn = functionWrapper(metric); - const context = { - type: 'kibana_datatable', - rows: [{ 'col-0-1': 0 }], - columns: [{ id: 'col-0-1', name: 'Count' }], - }; - const args = { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [ - { - from: 0, - to: 10000, - } - ], - labels: { - show: true, - }, - invertColors: false, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 60, - }, - font: { spec: { fontSize: 60 } }, - metrics: [ - { - accessor: 0, - format: { - id: 'number' - }, - params: {}, - aggType: 'count', - } - ] - }; - - it('returns an object with the correct structure', () => { - const actual = fn(context, args); - expect(actual).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/metrics/index.js b/src/legacy/core_plugins/metrics/index.js deleted file mode 100644 index 0771bf9726c2..000000000000 --- a/src/legacy/core_plugins/metrics/index.js +++ /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 { resolve } from 'path'; - -import { fieldsRoutes } from './server/routes/fields'; -import { visDataRoutes } from './server/routes/vis'; -import { SearchStrategiesRegister } from './server/lib/search_strategies/search_strategies_register'; - -export default function(kibana) { - return new kibana.Plugin({ - require: ['kibana', 'elasticsearch'], - - uiExports: { - visTypes: ['plugins/metrics/kbn_vis_types'], - interpreter: ['plugins/metrics/tsvb_fn'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - chartResolution: Joi.number().default(150), - minimumBucketSize: Joi.number().default(10), - }).default(); - }, - - init(server) { - fieldsRoutes(server); - visDataRoutes(server); - - SearchStrategiesRegister.init(server); - }, - }); -} diff --git a/src/legacy/core_plugins/metrics/index.ts b/src/legacy/core_plugins/metrics/index.ts new file mode 100644 index 000000000000..128f8d6a7294 --- /dev/null +++ b/src/legacy/core_plugins/metrics/index.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { resolve } from 'path'; +import { Legacy } from 'kibana'; +import { PluginInitializerContext } from 'src/core/server'; +import { CoreSetup } from 'src/core/server'; + +import { plugin } from './server/'; +import { CustomCoreSetup } from './server/plugin'; + +import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; + +const metricsPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => + new Plugin({ + id: 'metrics', + require: ['kibana', 'elasticsearch', 'visualizations', 'interpreter', 'data'], + publicDir: resolve(__dirname, 'public'), + uiExports: { + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + hacks: [resolve(__dirname, 'public/legacy')], + injectDefaultVars: server => ({}), + }, + init: (server: Legacy.Server) => { + const initializerContext = {} as PluginInitializerContext; + const core = { http: { server } } as CoreSetup & CustomCoreSetup; + + plugin(initializerContext).setup(core); + }, + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(true), + chartResolution: Joi.number().default(150), + minimumBucketSize: Joi.number().default(10), + }).default(); + }, + } as Legacy.PluginSpecOptions); + +// eslint-disable-next-line import/no-default-export +export default metricsPluginInitializer; diff --git a/src/legacy/core_plugins/metrics/public/components/annotations_editor.js b/src/legacy/core_plugins/metrics/public/components/annotations_editor.js index ce70b6d32df6..7236a778703a 100644 --- a/src/legacy/core_plugins/metrics/public/components/annotations_editor.js +++ b/src/legacy/core_plugins/metrics/public/components/annotations_editor.js @@ -29,8 +29,7 @@ import uuid from 'uuid'; import { IconSelect } from './icon_select'; import { YesNo } from './yes_no'; import { Storage } from 'ui/storage'; -import { data } from 'plugins/data/setup'; -const { QueryBarInput } = data.query.ui; +import { QueryBarInput } from 'plugins/data'; import { getDefaultQueryLanguage } from './lib/get_default_query_language'; import { diff --git a/src/legacy/core_plugins/metrics/public/components/color_picker.js b/src/legacy/core_plugins/metrics/public/components/color_picker.js index a2f50dafbbac..06868a73c6c8 100644 --- a/src/legacy/core_plugins/metrics/public/components/color_picker.js +++ b/src/legacy/core_plugins/metrics/public/components/color_picker.js @@ -24,49 +24,43 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { EuiIconTip } from '@elastic/eui'; import { CustomColorPicker } from './custom_color_picker'; -import { injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; -class ColorPickerUI extends Component { +export class ColorPicker extends Component { constructor(props) { super(props); this.state = { displayPicker: false, color: {}, }; - - this.handleClick = this.handleClick.bind(this); - this.handleChange = this.handleChange.bind(this); - this.handleClear = this.handleClear.bind(this); - this.handleClose = this.handleClose.bind(this); } - handleChange(color) { + handleChange = color => { const { rgb } = color; const part = {}; part[this.props.name] = `rgba(${rgb.r},${rgb.g},${rgb.b},${rgb.a})`; if (this.props.onChange) this.props.onChange(part); - } + }; - handleClick() { + handleClick = () => { this.setState({ displayPicker: !this.state.displayColorPicker }); - } + }; - handleClose() { + handleClose = () => { this.setState({ displayPicker: false }); - } + }; - handleClear() { + handleClear = () => { const part = {}; part[this.props.name] = null; this.props.onChange(part); - } + }; renderSwatch() { if (!this.props.value) { return ( - -
    -
    - - - -
    - -
    - -
    - - -

    - View 1 -

    -
    - -`; diff --git a/src/legacy/ui/public/inspector/ui/_index.scss b/src/legacy/ui/public/inspector/ui/_index.scss deleted file mode 100644 index e5cd29059a8a..000000000000 --- a/src/legacy/ui/public/inspector/ui/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './inspector'; diff --git a/src/legacy/ui/public/inspector/ui/_inspector.scss b/src/legacy/ui/public/inspector/ui/_inspector.scss deleted file mode 100644 index 88962905854e..000000000000 --- a/src/legacy/ui/public/inspector/ui/_inspector.scss +++ /dev/null @@ -1,3 +0,0 @@ -.kbnInspectorView--flex { - display: flex; -} diff --git a/src/legacy/ui/public/inspector/ui/index.ts b/src/legacy/ui/public/inspector/ui/index.ts deleted file mode 100644 index c295c8eca1af..000000000000 --- a/src/legacy/ui/public/inspector/ui/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 { InspectorView } from './inspector_view'; diff --git a/src/legacy/ui/public/inspector/ui/inspector_panel.d.ts b/src/legacy/ui/public/inspector/ui/inspector_panel.d.ts deleted file mode 100644 index 154b1a58f573..000000000000 --- a/src/legacy/ui/public/inspector/ui/inspector_panel.d.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 { ComponentClass } from 'react'; - -import { Adapters, InspectorViewDescription } from '../types'; - -interface InspectorPanelProps { - adapters: Adapters; - title?: string; - views: InspectorViewDescription[]; -} - -export const InspectorPanel: ComponentClass; diff --git a/src/legacy/ui/public/inspector/ui/inspector_panel.js b/src/legacy/ui/public/inspector/ui/inspector_panel.js deleted file mode 100644 index 26f100b89c17..000000000000 --- a/src/legacy/ui/public/inspector/ui/inspector_panel.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 { i18n } from '@kbn/i18n'; -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFlyoutHeader, - EuiTitle, -} from '@elastic/eui'; - -import { InspectorViewChooser } from './inspector_view_chooser'; - -function hasAdaptersChanged(oldAdapters, newAdapters) { - return Object.keys(oldAdapters).length !== Object.keys(newAdapters).length - || Object.keys(oldAdapters).some(key => oldAdapters[key] !== newAdapters[key]); -} - -const inspectorTitle = i18n.translate('common.ui.inspector.title', { - defaultMessage: 'Inspector', -}); - -class InspectorPanel extends Component { - - constructor(props) { - super(props); - this.state = { - selectedView: props.views[0], - views: props.views, - // Clone adapters array so we can validate that this prop never change - adapters: { ...props.adapters }, - }; - } - - static getDerivedStateFromProps(nextProps, prevState) { - if (hasAdaptersChanged(prevState.adapters, nextProps.adapters)) { - throw new Error('Adapters are not allowed to be changed on an open InspectorPanel.'); - } - const selectedViewMustChange = nextProps.views !== prevState.views - && !nextProps.views.includes(prevState.selectedView); - return { - views: nextProps.views, - selectedView: selectedViewMustChange ? nextProps.views[0] : prevState.selectedView, - }; - } - - onViewSelected = (view) => { - if (view !== this.state.selectedView) { - this.setState({ - selectedView: view - }); - } - }; - - renderSelectedPanel() { - return ( - - ); - } - - render() { - const { views, title } = this.props; - const { selectedView } = this.state; - - return ( - - - - - -

    { title }

    -
    -
    - - - -
    -
    - { this.renderSelectedPanel() } -
    - ); - } -} - -InspectorPanel.defaultProps = { - title: inspectorTitle, -}; - -InspectorPanel.propTypes = { - adapters: PropTypes.object.isRequired, - views: (props, propName, componentName) => { - if (!Array.isArray(props[propName]) || props[propName].length < 1) { - throw new Error( - `${propName} prop must be an array of at least one element in ${componentName}.` - ); - } - }, - title: PropTypes.string, -}; - -export { InspectorPanel }; diff --git a/src/legacy/ui/public/inspector/ui/inspector_panel.test.js b/src/legacy/ui/public/inspector/ui/inspector_panel.test.js deleted file mode 100644 index d4c72ba0132e..000000000000 --- a/src/legacy/ui/public/inspector/ui/inspector_panel.test.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 React from 'react'; -import { InspectorPanel } from './inspector_panel'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; - -describe('InspectorPanel', () => { - - let adapters; - let views; - - beforeEach(() => { - adapters = { - foodapter: { - foo() { return 42; } - }, - bardapter: { - - } - }; - views = [ - { - title: 'View 1', - order: 200, - component: () => (

    View 1

    ), - }, { - title: 'Foo View', - order: 100, - component: () => (

    Foo view

    ), - shouldShow(adapters) { - return adapters.foodapter; - } - }, { - title: 'Never', - order: 200, - component: () => null, - shouldShow() { - return false; - } - } - ]; - }); - - it('should render as expected', () => { - const component = mountWithIntl( - true} - views={views} - /> - ); - expect(component).toMatchSnapshot(); - }); - - it('should not allow updating adapters', () => { - const component = mountWithIntl( - true} - views={views} - /> - ); - adapters.notAllowed = {}; - expect(() => component.setProps({ adapters })).toThrow(); - }); -}); diff --git a/src/legacy/ui/public/inspector/ui/inspector_panel.tsx b/src/legacy/ui/public/inspector/ui/inspector_panel.tsx new file mode 100644 index 000000000000..92ed169bf15e --- /dev/null +++ b/src/legacy/ui/public/inspector/ui/inspector_panel.tsx @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable */ + +/** + * Do not use this, use NP `inspector` plugin instead. + * + * @deprecated + */ +export * from '../../../../../plugins/inspector/public/ui/inspector_panel'; diff --git a/src/legacy/ui/public/inspector/ui/inspector_view.tsx b/src/legacy/ui/public/inspector/ui/inspector_view.tsx deleted file mode 100644 index 45d7c95d0d7d..000000000000 --- a/src/legacy/ui/public/inspector/ui/inspector_view.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 classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import { EuiFlyoutBody } from '@elastic/eui'; - -/** - * The InspectorView component should be the top most element in every implemented - * inspector view. It makes sure, that the appropriate stylings are applied to the - * view. - */ -const InspectorView: React.SFC<{ useFlex?: boolean }> = ({ useFlex, children }) => { - const classes = classNames({ - 'kbnInspectorView--flex': Boolean(useFlex), - }); - return {children}; -}; - -InspectorView.propTypes = { - /** - * Set to true if the element should have display: flex set. - */ - useFlex: PropTypes.bool, -}; - -export { InspectorView }; diff --git a/src/legacy/ui/public/inspector/ui/inspector_view_chooser.js b/src/legacy/ui/public/inspector/ui/inspector_view_chooser.js deleted file mode 100644 index 2ab4c57ce8bb..000000000000 --- a/src/legacy/ui/public/inspector/ui/inspector_view_chooser.js +++ /dev/null @@ -1,134 +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 { FormattedMessage } from '@kbn/i18n/react'; -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiPopover, - EuiToolTip, -} from '@elastic/eui'; - -class InspectorViewChooser extends Component { - - state = { - isSelectorOpen: false - }; - - toggleSelector = () => { - this.setState((prev) => ({ - isSelectorOpen: !prev.isSelectorOpen - })); - }; - - closeSelector = () => { - this.setState({ - isSelectorOpen: false - }); - }; - - renderView = (view, index) => { - return ( - { - this.props.onViewSelected(view); - this.closeSelector(); - }} - toolTipContent={view.help} - toolTipPosition="left" - data-test-subj={`inspectorViewChooser${view.title}`} - > - {view.title} - - ); - } - - renderViewButton() { - return ( - - - - ); - } - - renderSingleView() { - return ( - - - - ); - } - - render() { - const { views } = this.props; - - if (views.length < 2) { - return this.renderSingleView(); - } - - const triggerButton = this.renderViewButton(); - - return ( - - - - ); - } -} - -InspectorViewChooser.propTypes = { - views: PropTypes.array.isRequired, - onViewSelected: PropTypes.func.isRequired, - selectedView: PropTypes.object.isRequired, -}; - -export { InspectorViewChooser }; diff --git a/src/legacy/ui/public/inspector/ui/inspector_view_chooser.tsx b/src/legacy/ui/public/inspector/ui/inspector_view_chooser.tsx new file mode 100644 index 000000000000..017e5c91095f --- /dev/null +++ b/src/legacy/ui/public/inspector/ui/inspector_view_chooser.tsx @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable */ + +/** + * Do not use this, use NP `inspector` plugin instead. + * + * @deprecated + */ +export * from '../../../../../plugins/inspector/public/ui/inspector_view_chooser'; diff --git a/src/legacy/ui/public/inspector/view_registry.ts b/src/legacy/ui/public/inspector/view_registry.ts index a4be902fc8ee..199087960ba8 100644 --- a/src/legacy/ui/public/inspector/view_registry.ts +++ b/src/legacy/ui/public/inspector/view_registry.ts @@ -17,66 +17,19 @@ * under the License. */ -import { EventEmitter } from 'events'; -import { Adapters, InspectorViewDescription } from './types'; +import { npSetup } from 'ui/new_platform'; +export { InspectorViewDescription } from './types'; /** - * @callback viewShouldShowFunc - * @param {object} adapters - A list of adapters to check whether or not this view - * should be shown for. - * @returns {boolean} true - if this view should be shown for the given adapters. - */ - -/** - * A registry that will hold inspector views. - */ -class InspectorViewRegistry extends EventEmitter { - private views: InspectorViewDescription[] = []; - - /** - * Register a new inspector view to the registry. Check the README.md in the - * inspector directory for more information of the object format to register - * here. This will also emit a 'change' event on the registry itself. - * - * @param {InspectorViewDescription} view - The view description to add to the registry. - */ - public register(view: InspectorViewDescription): void { - if (!view) { - return; - } - this.views.push(view); - // Keep registry sorted by the order property - this.views.sort((a, b) => (a.order || Number.MAX_VALUE) - (b.order || Number.MAX_VALUE)); - this.emit('change'); - } - - /** - * Retrieve all views currently registered with the registry. - * @returns {InspectorViewDescription[]} A by `order` sorted list of all registered - * inspector views. - */ - public getAll(): InspectorViewDescription[] { - return this.views; - } - - /** - * Retrieve all registered views, that want to be visible for the specified adapters. - * @param {object} adapters - an adapter configuration - * @returns {InspectorViewDescription[]} All inespector view descriptions visible - * for the specific adapters. - */ - public getVisible(adapters?: Adapters): InspectorViewDescription[] { - if (!adapters) { - return []; - } - return this.views.filter(view => !view.shouldShow || view.shouldShow(adapters)); - } -} - -/** - * The global view registry. In the long run this should be solved by a registry - * system introduced by the new platform instead, to not keep global state like that. + * Do not use this, instead use `inspector` plugin directly. + * + * ```ts + * import { npSetup } from 'ui/new_platform'; + * + * npSetup.plugins.inspector.registerView(view); + * ``` + * + * @deprecated */ -const viewRegistry = new InspectorViewRegistry(); - -export { viewRegistry, InspectorViewRegistry, InspectorViewDescription }; +export const viewRegistry = + npSetup.plugins.inspector.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.views; diff --git a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js index c18430173568..997454d0798a 100644 --- a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js @@ -56,7 +56,7 @@ import _ from 'lodash'; import angular from 'angular'; import './timepicker'; -import '../watch_multi'; +import '../directives/watch_multi'; import '../directives/input_focus'; import { uiModules } from '../modules'; import template from './kbn_top_nav.html'; diff --git a/src/legacy/ui/public/listen/__tests__/listen.js b/src/legacy/ui/public/listen/__tests__/listen.js deleted file mode 100644 index 84838549b2a3..000000000000 --- a/src/legacy/ui/public/listen/__tests__/listen.js +++ /dev/null @@ -1,96 +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 '..'; -import { EventsProvider } from '../../events'; - -describe('listen component', function () { - - let $rootScope; - let Events; - - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function ($injector, Private) { - $rootScope = $injector.get('$rootScope'); - Events = Private(EventsProvider); - })); - - it('exposes the $listen method on all scopes', function () { - expect($rootScope.$listen).to.be.a('function'); - expect($rootScope.$new().$listen).to.be.a('function'); - }); - - it('binds to an event emitter', function () { - const emitter = new Events(); - const $scope = $rootScope.$new(); - - function handler() {} - $scope.$listen(emitter, 'hello', handler); - - expect(emitter._listeners.hello).to.have.length(1); - expect(emitter._listeners.hello[0].handler).to.be(handler); - }); - - it('binds to $scope, waiting for the destroy event', function () { - const emitter = new Events(); - const $scope = $rootScope.$new(); - - sinon.stub($scope, '$on'); - sinon.stub($rootScope, '$on'); - - function handler() {} - $scope.$listen(emitter, 'hello', handler); - - expect($rootScope.$on).to.have.property('callCount', 0); - expect($scope.$on).to.have.property('callCount', 1); - - const call = $scope.$on.firstCall; - expect(call.args[0]).to.be('$destroy'); - expect(call.args[1]).to.be.a('function'); - }); - - it('unbinds the event handler when $destroy is triggered', function () { - const emitter = new Events(); - const $scope = $rootScope.$new(); - - sinon.stub($scope, '$on'); - sinon.stub(emitter, 'off'); - - // set the listener - function handler() {} - $scope.$listen(emitter, 'hello', handler); - - // get the unbinder that was registered to $scope - const unbinder = $scope.$on.firstCall.args[1]; - - // call the unbinder - expect(emitter.off).to.have.property('callCount', 0); - unbinder(); - expect(emitter.off).to.have.property('callCount', 1); - - // check that the off args were as expected - const call = emitter.off.firstCall; - expect(call.args[0]).to.be('hello'); - expect(call.args[1]).to.be(handler); - }); -}); diff --git a/src/legacy/ui/public/listen/listen.js b/src/legacy/ui/public/listen/listen.js deleted file mode 100644 index 99bc17352060..000000000000 --- a/src/legacy/ui/public/listen/listen.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 { uiModules } from '../modules'; - -uiModules.get('kibana') - .run(function ($rootScope) { - - /** - * Helper that registers an event listener, and removes that listener when - * the $scope is destroyed. - * - * @param {SimpleEmitter} emitter - the event emitter to listen to - * @param {string} eventName - the event name - * @param {Function} handler - the event handler - * @return {undefined} - */ - $rootScope.constructor.prototype.$listen = function (emitter, eventName, handler) { - emitter.on(eventName, handler); - this.$on('$destroy', function () { - emitter.off(eventName, handler); - }); - }; - - /** - * Helper that registers an event listener, and removes that listener when - * the $scope is destroyed. Handler is executed inside $evalAsync, ensuring digest cycle is run after the handler - * - * @param {SimpleEmitter} emitter - the event emitter to listen to - * @param {string} eventName - the event name - * @param {Function} handler - the event handler - * @return {undefined} - */ - $rootScope.constructor.prototype.$listenAndDigestAsync = function (emitter, eventName, handler) { - const evalAsyncWrappedHandler = (...args) => { - this.$evalAsync(() => handler(args)); - }; - this.$listen(emitter, eventName, evalAsyncWrappedHandler); - }; - - }); diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts index 439d15880265..838d94264848 100644 --- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts +++ b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts @@ -17,16 +17,20 @@ * under the License. */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ import { coreMock } from '../../../../../core/public/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; +import { inspectorPluginMock } from '../../../../../plugins/inspector/public/mocks'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ export const pluginsMock = { createSetup: () => ({ data: dataPluginMock.createSetupContract(), + inspector: inspectorPluginMock.createSetupContract(), }), createStart: () => ({ data: dataPluginMock.createStartContract(), + inspector: inspectorPluginMock.createStartContract(), }), }; 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 new file mode 100644 index 000000000000..ecbf514892a2 --- /dev/null +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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'; + +export const npSetup = { + core: {}, + plugins: { + data: { + expressions: { + registerFunction: sinon.fake(), + registerRenderer: sinon.fake(), + registerType: sinon.fake(), + }, + }, + inspector: { + registerView: () => undefined, + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { + views: { + register: () => undefined, + }, + }, + }, + }, +}; + +export const npStart = { + core: {}, + plugins: { + data: {}, + inspector: { + isAvailable: () => false, + open: () => ({ + onClose: Promise.resolve(undefined), + close: () => Promise.resolve(undefined), + }), + }, + }, +}; + +export function __setup__(coreSetup) { + npSetup.core = coreSetup; +} + +export function __start__(coreStart) { + npStart.core = coreStart; +} diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index cfcf99fcbc9f..5e0eb2feeb45 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -18,13 +18,19 @@ */ import { InternalCoreSetup, InternalCoreStart } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; +import { + Setup as InspectorSetup, + Start as InspectorStart, +} from '../../../../plugins/inspector/public'; export interface PluginsSetup { data: ReturnType; + inspector: InspectorSetup; } export interface PluginsStart { data: ReturnType; + inspector: InspectorStart; } export const npSetup = { diff --git a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js index 6539f8c03413..766ed44a4c0f 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js +++ b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js @@ -20,7 +20,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; import { findObjectByTitle } from '../find_object_by_title'; -import { SimpleSavedObject } from '../simple_saved_object'; +import { SimpleSavedObject } from '../../../../../core/public'; describe('findObjectByTitle', () => { const sandbox = sinon.createSandbox(); diff --git a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js index bbb7580c0a5a..64d030bd7c7e 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js @@ -23,7 +23,7 @@ import sinon from 'sinon'; import BluebirdPromise from 'bluebird'; import { SavedObjectProvider } from '../saved_object'; -import { IndexPattern } from '../../index_patterns/_index_pattern'; +import { IndexPattern } from '../../index_patterns'; import { SavedObjectsClientProvider } from '../saved_objects_client_provider'; import { InvalidJSONProperty } from '../../errors'; diff --git a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js index a6583b97972a..f2fc9bfe232e 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; -import { SimpleSavedObject } from '../simple_saved_object'; +import { SimpleSavedObject } from '../../../../../core/public'; describe('SimpleSavedObject', () => { it('persists type and id', () => { diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx index a5930ee38a66..9169286fb417 100644 --- a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx @@ -46,7 +46,7 @@ import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { i18n } from '@kbn/i18n'; import { SavedObjectAttributes } from 'src/core/server'; -import { SimpleSavedObject } from '../simple_saved_object'; +import { SimpleSavedObject } from 'src/core/public'; // TODO the typings for EuiListGroup are incorrect - maxWidth is missing. This can be removed when the types are adjusted const FixedEuiListGroup = (EuiListGroup as any) as React.FunctionComponent< diff --git a/src/legacy/ui/public/saved_objects/find_object_by_title.ts b/src/legacy/ui/public/saved_objects/find_object_by_title.ts index e27326249f5c..d6f11bcb8095 100644 --- a/src/legacy/ui/public/saved_objects/find_object_by_title.ts +++ b/src/legacy/ui/public/saved_objects/find_object_by_title.ts @@ -19,19 +19,19 @@ import { find } from 'lodash'; import { SavedObjectAttributes } from 'src/core/server'; -import { SavedObjectsClient } from './saved_objects_client'; -import { SimpleSavedObject } from './simple_saved_object'; +import { SavedObjectsClientContract } from 'src/core/public'; +import { SimpleSavedObject } from 'src/core/public'; /** * Returns an object matching a given title * - * @param savedObjectsClient {SavedObjectsClient} + * @param savedObjectsClient {SavedObjectsClientContract} * @param type {string} * @param title {string} * @returns {Promise} */ export function findObjectByTitle( - savedObjectsClient: SavedObjectsClient, + savedObjectsClient: SavedObjectsClientContract, type: string, title: string ): Promise | void> { diff --git a/src/legacy/ui/public/saved_objects/index.ts b/src/legacy/ui/public/saved_objects/index.ts index 31222bb9f5eb..8076213f62e9 100644 --- a/src/legacy/ui/public/saved_objects/index.ts +++ b/src/legacy/ui/public/saved_objects/index.ts @@ -17,10 +17,8 @@ * under the License. */ -export { SavedObjectsClient } from './saved_objects_client'; export { SavedObjectRegistryProvider } from './saved_object_registry'; export { SavedObjectsClientProvider } from './saved_objects_client_provider'; // @ts-ignore export { SavedObjectLoader } from './saved_object_loader'; -export { SimpleSavedObject } from './simple_saved_object'; export { findObjectByTitle } from './find_object_by_title'; diff --git a/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts b/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts deleted file mode 100644 index fed3b8807cdd..000000000000 --- a/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts +++ /dev/null @@ -1,357 +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/kfetch', () => ({})); - -import * as sinon from 'sinon'; -import { SavedObjectsFindOptions } from 'src/core/server'; -import { SavedObjectsClient } from './saved_objects_client'; -import { SimpleSavedObject } from './simple_saved_object'; - -describe('SavedObjectsClient', () => { - const doc = { - id: 'AVwSwFxtcMV38qjDZoQg', - type: 'config', - attributes: { title: 'Example title' }, - version: 'foo', - }; - - let kfetchStub: sinon.SinonStub; - let savedObjectsClient: SavedObjectsClient; - beforeEach(() => { - kfetchStub = sinon.stub(); - require('ui/kfetch').kfetch = async (...args: any[]) => { - return kfetchStub(...args); - }; - savedObjectsClient = new SavedObjectsClient(); - }); - - describe('#get', () => { - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'POST', - pathname: `/api/saved_objects/_bulk_get`, - query: undefined, - body: sinon.match.any, - }) - .returns(Promise.resolve({ saved_objects: [doc] })); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.get('index-pattern', 'logstash-*')).toBeInstanceOf(Promise); - }); - - test('requires type', async () => { - try { - await savedObjectsClient.get(undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe('requires type and id'); - } - }); - - test('requires id', async () => { - try { - await savedObjectsClient.get('index-pattern', undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe('requires type and id'); - } - }); - - test('resolves with instantiated SavedObject', async () => { - const response = await savedObjectsClient.get(doc.type, doc.id); - expect(response).toBeInstanceOf(SimpleSavedObject); - expect(response.type).toBe('config'); - expect(response.get('title')).toBe('Example title'); - }); - - test('makes HTTP call', async () => { - await savedObjectsClient.get(doc.type, doc.id); - sinon.assert.calledOnce(kfetchStub); - }); - - test('handles HTTP call when it fails', async () => { - kfetchStub - .withArgs({ - method: 'POST', - pathname: `/api/saved_objects/_bulk_get`, - query: undefined, - body: sinon.match.any, - }) - .rejects(new Error('Request failed')); - try { - await savedObjectsClient.get(doc.type, doc.id); - throw new Error('should have error'); - } catch (e) { - expect(e.message).toBe('Request failed'); - } - }); - }); - - describe('#delete', () => { - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'DELETE', - pathname: `/api/saved_objects/index-pattern/logstash-*`, - query: undefined, - body: undefined, - }) - .returns(Promise.resolve({})); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).toBeInstanceOf(Promise); - }); - - test('requires type', async () => { - try { - await savedObjectsClient.delete(undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe('requires type and id'); - } - }); - - test('requires id', async () => { - try { - await savedObjectsClient.delete('index-pattern', undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe('requires type and id'); - } - }); - - test('makes HTTP call', () => { - savedObjectsClient.delete('index-pattern', 'logstash-*'); - sinon.assert.calledOnce(kfetchStub); - }); - }); - - describe('#update', () => { - const requireMessage = 'requires type, id and attributes'; - - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'PUT', - pathname: `/api/saved_objects/index-pattern/logstash-*`, - query: undefined, - body: sinon.match.any, - }) - .returns(Promise.resolve({ data: 'api-response' })); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.update('index-pattern', 'logstash-*', {})).toBeInstanceOf(Promise); - }); - - test('requires type', async () => { - try { - await savedObjectsClient.update(undefined as any, undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe(requireMessage); - } - }); - - test('requires id', async () => { - try { - await savedObjectsClient.update('index-pattern', undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe(requireMessage); - } - }); - - test('requires attributes', async () => { - try { - await savedObjectsClient.update('index-pattern', 'logstash-*', undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe(requireMessage); - } - }); - - test('makes HTTP call', () => { - const attributes = { foo: 'Foo', bar: 'Bar' }; - const body = { attributes, version: 'foo' }; - const options = { version: 'foo' }; - - savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options); - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - body: JSON.stringify(body), - }) - ); - }); - }); - - describe('#create', () => { - const requireMessage = 'requires type and attributes'; - - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'POST', - pathname: `/api/saved_objects/index-pattern`, - query: undefined, - body: sinon.match.any, - }) - .returns(Promise.resolve({})); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.create('index-pattern', {})).toBeInstanceOf(Promise); - }); - - test('requires type', async () => { - try { - await savedObjectsClient.create(undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe(requireMessage); - } - }); - - test('allows for id to be provided', () => { - const attributes = { foo: 'Foo', bar: 'Bar' }; - const path = `/api/saved_objects/index-pattern/myId`; - kfetchStub - .withArgs({ - method: 'POST', - pathname: path, - query: undefined, - body: sinon.match.any, - }) - .returns(Promise.resolve({})); - - savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }); - - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - pathname: path, - }) - ); - }); - - test('makes HTTP call', () => { - const attributes = { foo: 'Foo', bar: 'Bar' }; - savedObjectsClient.create('index-pattern', attributes); - - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - pathname: sinon.match.string, - body: JSON.stringify({ attributes }), - }) - ); - }); - }); - - describe('#bulk_create', () => { - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'POST', - pathname: `/api/saved_objects/_bulk_create`, - query: sinon.match.any, - body: sinon.match.any, - }) - .returns(Promise.resolve({ saved_objects: [doc] })); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.bulkCreate([doc], {})).toBeInstanceOf(Promise); - }); - - test('resolves with instantiated SavedObjects', async () => { - const response = await savedObjectsClient.bulkCreate([doc], {}); - expect(response).toHaveProperty('savedObjects'); - expect(response.savedObjects.length).toBe(1); - expect(response.savedObjects[0]).toBeInstanceOf(SimpleSavedObject); - }); - - test('makes HTTP call', async () => { - await savedObjectsClient.bulkCreate([doc], {}); - sinon.assert.calledOnce(kfetchStub); - }); - }); - - describe('#find', () => { - const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' }; - - beforeEach(() => { - kfetchStub.returns(Promise.resolve({ saved_objects: [object] })); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.find()).toBeInstanceOf(Promise); - }); - - test('accepts type', () => { - const body = { type: 'index-pattern', invalid: true }; - - savedObjectsClient.find(body); - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - pathname: `/api/saved_objects/_find`, - query: { type: 'index-pattern', invalid: true }, - }) - ); - }); - - test('accepts fields', () => { - const body = { fields: ['title', 'description'] }; - - savedObjectsClient.find(body); - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - pathname: `/api/saved_objects/_find`, - query: { fields: ['title', 'description'] }, - }) - ); - }); - - test('accepts pagination params', () => { - const options: SavedObjectsFindOptions = { perPage: 10, page: 6 }; - - savedObjectsClient.find(options); - sinon.assert.calledOnce(kfetchStub); - sinon.assert.alwaysCalledWith( - kfetchStub, - sinon.match({ - pathname: `/api/saved_objects/_find`, - query: { per_page: 10, page: 6 }, - }) - ); - }); - }); -}); diff --git a/src/legacy/ui/public/saved_objects/saved_objects_client.ts b/src/legacy/ui/public/saved_objects/saved_objects_client.ts deleted file mode 100644 index d8fb3af1c66a..000000000000 --- a/src/legacy/ui/public/saved_objects/saved_objects_client.ts +++ /dev/null @@ -1,361 +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, pick, throttle } from 'lodash'; -import { resolve as resolveUrl } from 'url'; - -import { - SavedObject, - SavedObjectAttributes, - SavedObjectReference, - SavedObjectsClientContract as SavedObjectsApi, - SavedObjectsFindOptions, - SavedObjectsMigrationVersion, -} from 'src/core/server'; -import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; -import { kfetch, KFetchQuery } from '../kfetch'; -import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../utils/case_conversion'; -import { SimpleSavedObject } from './simple_saved_object'; - -interface RequestParams { - method: 'POST' | 'GET' | 'PUT' | 'DELETE'; - path: string; - query?: KFetchQuery; - body?: object; -} - -interface CreateOptions { - id?: string; - overwrite?: boolean; - migrationVersion?: SavedObjectsMigrationVersion; - references?: SavedObjectReference[]; -} - -interface BulkCreateOptions - extends CreateOptions { - type: string; - attributes: T; -} - -interface UpdateOptions { - version?: string; - migrationVersion?: SavedObjectsMigrationVersion; - references?: SavedObjectReference[]; -} - -interface BatchResponse { - savedObjects: Array>; -} - -interface FindResults - extends BatchResponse { - total: number; - perPage: number; - page: number; -} - -interface BatchQueueEntry { - type: string; - id: string; - resolve: (value: SimpleSavedObject | SavedObject) => void; - reject: (reason?: any) => void; -} - -const join = (...uriComponents: Array) => - uriComponents - .filter((comp): comp is string => Boolean(comp)) - .map(encodeURIComponent) - .join('/'); - -/** - * Interval that requests are batched for - * @type {integer} - */ -const BATCH_INTERVAL = 100; - -const API_BASE_URL = '/api/saved_objects/'; - -/** - * The SavedObjectsClient class acts as a generic data fetcher - * and data saver for saved objects regardless of type. - * - * If possible, this class should be used to load saved objects - * instead of the SavedObjectLoader class which implements some - * additional functionality. - */ -export class SavedObjectsClient { - /** - * Throttled processing of get requests into bulk requests at 100ms interval - */ - private processBatchQueue = throttle( - () => { - const queue = cloneDeep(this.batchQueue); - this.batchQueue = []; - - this.bulkGet(queue) - .then(({ savedObjects }) => { - queue.forEach(queueItem => { - const foundObject = savedObjects.find(savedObject => { - return savedObject.id === queueItem.id && savedObject.type === queueItem.type; - }); - - if (!foundObject) { - return queueItem.resolve(this.createSavedObject(pick(queueItem, ['id', 'type']))); - } - - queueItem.resolve(foundObject); - }); - }) - .catch(err => { - queue.forEach(queueItem => { - queueItem.reject(err); - }); - }); - }, - BATCH_INTERVAL, - { leading: false } - ); - - private batchQueue: BatchQueueEntry[]; - - constructor() { - this.batchQueue = []; - } - - /** - * Persists an object - * - * @param {string} type - * @param {object} [attributes={}] - * @param {object} [options={}] - * @property {string} [options.id] - force id on creation, not recommended - * @property {boolean} [options.overwrite=false] - * @property {object} [options.migrationVersion] - * @returns - */ - public create = ( - type: string, - attributes: T, - options: CreateOptions = {} - ): Promise> => { - if (!type || !attributes) { - return Promise.reject(new Error('requires type and attributes')); - } - - const path = this.getPath([type, options.id]); - const query = { - overwrite: options.overwrite, - }; - - const createRequest: Promise> = this.request({ - method: 'POST', - path, - query, - body: { - attributes, - migrationVersion: options.migrationVersion, - references: options.references, - }, - }); - - return createRequest - .then(resp => this.createSavedObject(resp)) - .catch((error: object) => { - if (isAutoCreateIndexError(error)) { - showAutoCreateIndexErrorPage(); - } - - throw error; - }); - }; - - /** - * Creates multiple documents at once - * - * @param {array} objects - [{ type, id, attributes, references, migrationVersion }] - * @param {object} [options={}] - * @property {boolean} [options.overwrite=false] - * @returns The result of the create operation containing created saved objects. - */ - public bulkCreate = (objects: BulkCreateOptions[] = [], options: KFetchQuery = {}) => { - const path = this.getPath(['_bulk_create']); - const query = pick(options, ['overwrite']) as Pick; - - const request: ReturnType = this.request({ - method: 'POST', - path, - query, - body: objects, - }); - return request.then(resp => { - resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp) as BatchResponse; - }); - }; - - /** - * Deletes an object - * - * @param type - * @param id - * @returns - */ - public delete = (type: string, id: string): ReturnType => { - if (!type || !id) { - return Promise.reject(new Error('requires type and id')); - } - - return this.request({ method: 'DELETE', path: this.getPath([type, id]) }); - }; - - /** - * Search for objects - * - * @param {object} [options={}] - * @property {string} options.type - * @property {string} options.search - * @property {string} options.searchFields - see Elasticsearch Simple Query String - * Query field argument for more information - * @property {integer} [options.page=1] - * @property {integer} [options.perPage=20] - * @property {array} options.fields - * @property {object} [options.hasReference] - { type, id } - * @returns A find result with objects matching the specified search. - */ - public find = ( - options: SavedObjectsFindOptions = {} - ): Promise> => { - const path = this.getPath(['_find']); - const query = keysToSnakeCaseShallow(options); - - const request: ReturnType = this.request({ - method: 'GET', - path, - query, - }); - return request.then(resp => { - resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp) as FindResults; - }); - }; - - /** - * Fetches a single object - * - * @param {string} type - * @param {string} id - * @returns The saved object for the given type and id. - */ - public get = ( - type: string, - id: string - ): Promise> => { - if (!type || !id) { - return Promise.reject(new Error('requires type and id')); - } - - return new Promise((resolve, reject) => { - this.batchQueue.push({ type, id, resolve, reject } as BatchQueueEntry); - this.processBatchQueue(); - }); - }; - - /** - * Returns an array of objects by id - * - * @param {array} objects - an array ids, or an array of objects containing id and optionally type - * @returns The saved objects with the given type and ids requested - * @example - * - * bulkGet([ - * { id: 'one', type: 'config' }, - * { id: 'foo', type: 'index-pattern' } - * ]) - */ - public bulkGet = (objects: Array<{ id: string; type: string }> = []) => { - const path = this.getPath(['_bulk_get']); - const filteredObjects = objects.map(obj => pick(obj, ['id', 'type'])); - - const request: ReturnType = this.request({ - method: 'POST', - path, - body: filteredObjects, - }); - return request.then(resp => { - resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp) as BatchResponse; - }); - }; - - /** - * Updates an object - * - * @param {string} type - * @param {string} id - * @param {object} attributes - * @param {object} options - * @prop {integer} options.version - ensures version matches that of persisted object - * @prop {object} options.migrationVersion - The optional migrationVersion of this document - * @returns - */ - public update( - type: string, - id: string, - attributes: T, - { version, migrationVersion, references }: UpdateOptions = {} - ): Promise> { - if (!type || !id || !attributes) { - return Promise.reject(new Error('requires type, id and attributes')); - } - - const path = this.getPath([type, id]); - const body = { - attributes, - migrationVersion, - references, - version, - }; - - return this.request({ - method: 'PUT', - path, - body, - }).then((resp: SavedObject) => { - return this.createSavedObject(resp); - }); - } - - private createSavedObject( - options: SavedObject - ): SimpleSavedObject { - return new SimpleSavedObject(this, options); - } - - private getPath(path: Array): string { - return resolveUrl(API_BASE_URL, join(...path)); - } - - private request({ method, path, query, body }: RequestParams) { - if (method === 'GET' && body) { - return Promise.reject(new Error('body not permitted for GET requests')); - } - - return kfetch({ method, pathname: path, query, body: JSON.stringify(body) }); - } -} diff --git a/src/legacy/ui/public/saved_objects/saved_objects_client_provider.ts b/src/legacy/ui/public/saved_objects/saved_objects_client_provider.ts index 1ef0d07135fd..0375eb21c19f 100644 --- a/src/legacy/ui/public/saved_objects/saved_objects_client_provider.ts +++ b/src/legacy/ui/public/saved_objects/saved_objects_client_provider.ts @@ -17,9 +17,9 @@ * under the License. */ +import { SavedObjectsClient } from 'src/core/public'; import chrome from '../chrome'; import { PromiseService } from '../promises'; -import { SavedObjectsClient } from './saved_objects_client'; type Args any> = T extends (...args: infer X) => any ? X : never; diff --git a/src/legacy/ui/public/styles/_legacy/components/_sidebar.scss b/src/legacy/ui/public/styles/_legacy/components/_sidebar.scss index 9dbc3608d27b..daa3ad512e17 100644 --- a/src/legacy/ui/public/styles/_legacy/components/_sidebar.scss +++ b/src/legacy/ui/public/styles/_legacy/components/_sidebar.scss @@ -111,8 +111,7 @@ } .index-pattern-selection { - padding: $euiSizeS; - padding-bottom: 0; + padding: 0 $euiSizeS; } .index-pattern-selection .ui-select-choices { diff --git a/src/legacy/ui/public/styles/_legacy/components/_table.scss b/src/legacy/ui/public/styles/_legacy/components/_table.scss index 94f30fa1f349..e7c1bda829f0 100644 --- a/src/legacy/ui/public/styles/_legacy/components/_table.scss +++ b/src/legacy/ui/public/styles/_legacy/components/_table.scss @@ -33,14 +33,14 @@ table { button.fa-sort-down, i.fa-sort-asc, i.fa-sort-down { - color: $euiColorLightShade; + color: $euiColorPrimary; } button.fa-sort-desc, button.fa-sort-up, i.fa-sort-desc, i.fa-sort-up { - color: $euiColorLightShade; + color: $euiColorPrimary; } } } diff --git a/src/legacy/ui/public/timefilter/get_time.test.ts b/src/legacy/ui/public/timefilter/get_time.test.ts index 7360fcc7d064..0602f61d003e 100644 --- a/src/legacy/ui/public/timefilter/get_time.test.ts +++ b/src/legacy/ui/public/timefilter/get_time.test.ts @@ -43,7 +43,7 @@ describe('get_time', () => { filterable: true, }, ], - }, + } as any, { from: 'now-60y', to: 'now' } ) as Filter; expect(filter.range.date).to.eql({ diff --git a/src/legacy/ui/public/timefilter/index.d.ts b/src/legacy/ui/public/timefilter/index.d.ts index db6ead9d17a1..ce1ae86bb93b 100644 --- a/src/legacy/ui/public/timefilter/index.d.ts +++ b/src/legacy/ui/public/timefilter/index.d.ts @@ -18,3 +18,4 @@ */ export { timefilter, Timefilter } from './timefilter'; +export { timeHistory, TimeRange } from './time_history'; diff --git a/src/legacy/ui/public/validated_range/index.d.ts b/src/legacy/ui/public/validated_range/index.d.ts new file mode 100644 index 000000000000..50cacbc517be --- /dev/null +++ b/src/legacy/ui/public/validated_range/index.d.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 React from 'react'; +import { EuiRangeProps } from '@elastic/eui'; + +export class ValidatedDualRange extends React.Component { + allowEmptyRange?: boolean; +} diff --git a/src/legacy/ui/public/validated_range/validated_dual_range.js b/src/legacy/ui/public/validated_range/validated_dual_range.js index 7adbb578a237..088d36dbb18f 100644 --- a/src/legacy/ui/public/validated_range/validated_dual_range.js +++ b/src/legacy/ui/public/validated_range/validated_dual_range.js @@ -66,6 +66,9 @@ export class ValidatedDualRange extends Component { render() { const { + compressed, + fullWidth, + label, value, // eslint-disable-line no-unused-vars onChange, // eslint-disable-line no-unused-vars allowEmptyRange, // eslint-disable-line no-unused-vars @@ -75,10 +78,15 @@ export class ValidatedDualRange extends Component { return ( { diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_client.js b/src/legacy/ui/public/vis/__tests__/map/ems_client.js index 2fd2bf1bf92a..a6d71e4408f9 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_client.js +++ b/src/legacy/ui/public/vis/__tests__/map/ems_client.js @@ -19,6 +19,7 @@ import expect from '@kbn/expect'; import { getEMSClient } from './ems_client_util'; +import EMS_STYLE_BRIGHT_PROXIED from './ems_mocks/sample_style_bright_proxied.json'; describe('ems_client', () => { @@ -30,7 +31,6 @@ describe('ems_client', () => { expect(tiles.length).to.be(3); - const tileService = tiles[0]; expect(await tileService.getUrlTemplate()).to.be('https://raster-style.foobar/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=7.x.x'); @@ -186,5 +186,31 @@ describe('ems_client', () => { }); + it('should prepend proxypath', async () => { + + + const emsClient = getEMSClient({ + proxyPath: 'http://proxy.com/foobar', + manifestServiceUrl: 'http://proxy.com/foobar/manifest' + }); + + //should prepend the proxypath to all urls, for tiles and files + const tmsServices = await emsClient.getTMSServices(); + expect(tmsServices.length).to.be(1); + const tmsService = tmsServices[0]; + tmsService._getRasterStyleJson = () => { + return EMS_STYLE_BRIGHT_PROXIED; + }; + const urlTemplate = await tmsServices[0].getUrlTemplate(); + expect(urlTemplate).to.be('http://proxy.com/foobar/tiles/raster/osm_bright/{x}/{y}/{z}/.jpg?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=7.x.x'); + + const fileLayers = await emsClient.getFileLayers(); + expect(fileLayers.length).to.be(1); + const fileLayer = fileLayers[0]; + expect(fileLayer.getDefaultFormatUrl()).to.be('http://proxy.com/foobar/files/world_countries.json?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=7.x.x'); + + }); + + }); diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_client_util.js b/src/legacy/ui/public/vis/__tests__/map/ems_client_util.js index bba5e642f4c7..53cbdc9d1301 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_client_util.js +++ b/src/legacy/ui/public/vis/__tests__/map/ems_client_util.js @@ -27,6 +27,10 @@ import EMS_STYLE_ROAD_MAP_BRIGHT from './ems_mocks/sample_style_bright'; import EMS_STYLE_ROAD_MAP_DESATURATED from './ems_mocks/sample_style_desaturated'; import EMS_STYLE_DARK_MAP from './ems_mocks/sample_style_dark'; +import EMS_CATALOGUE_PROXIED from './ems_mocks/sample_manifest_proxied.json'; +import EMS_FILES_PROXIED from './ems_mocks/sample_files_proxied.json'; +import EMS_TILES_PROXIED from './ems_mocks/sample_tiles_proxied.json'; + export function getEMSClient(options = {}) { const emsClient = new EMSClient({ @@ -54,6 +58,12 @@ export function getEMSClient(options = {}) { } else if (url.includes('dark-matter')) { return EMS_STYLE_DARK_MAP; } + } else if (url.startsWith('http://proxy.com/foobar/manifest')) { + return EMS_CATALOGUE_PROXIED; + } else if (url.startsWith('http://proxy.com/foobar/files')) { + return EMS_FILES_PROXIED; + } else if (url.startsWith('http://proxy.com/foobar/tiles')) { + return EMS_TILES_PROXIED; } }; return emsClient; diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files_proxied.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files_proxied.json new file mode 100644 index 000000000000..ea4fbf22b88c --- /dev/null +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files_proxied.json @@ -0,0 +1,409 @@ +{ + "layers": [ + { + "layer_id": "world_countries", + "created_at": "2017-04-26T17:12:15.978370", + "attribution": [ + { + "label": { + "en": "Made with NaturalEarth" + }, + "url": { + "en": "http://www.naturalearthdata.com/about/terms-of-use" + } + }, + { + "label": { + "en": "Elastic Maps Service" + }, + "url": { + "en": "https://www.elastic.co/elastic-maps-service" + } + } + ], + "formats": [ + { + "type": "geojson", + "url": "/files/world_countries.json", + "legacy_default": true + } + ], + "fields": [ + { + "type": "id", + "id": "iso2", + "label": { + "en": "ISO 3166-1 alpha-2 code", + "af": "landkode (ISO 3166-1 alpha-2)", + "ar": "أيزو 3166-1 حرفي-2", + "be": "код краіны (ISO 3166-1 alpha-2)", + "be-tarask": "код краіны (ISO 3166-1 alpha-2)", + "bn": "রাষ্ট্রীয় কোড (আইএসও ৩১৬৬-১ আলফা-২)", + "br": "kod ar vro (ISO 3166-1 alpha-2)", + "bs": "ISO 3166-1 alpha-2 kod", + "ca": "codi de país (ISO 3166-1 alpha-2)", + "ckb": "کۆدی وڵات (ISO 3166-1 alpha-2)", + "cs": "kód země (ISO 3166-1 alpha-2)", + "da": "landekode (ISO 3166-1 alfa-2)", + "de": "Ländercode (ISO 3166-1 alpha-2)", + "el": "ISO 3166-1 alpha-2", + "en-gb": "country code (ISO 3166-1 alpha-2)", + "eo": "landokodo (ISO 3166-1 alfa-2)", + "es": "código ISO 3166-1 de dos letras", + "et": "maakood (ISO 3166-1 alpha-2)", + "eu": "ISO 3166-1 alpha-2", + "fa": "کد کشور (ایزو ۳۱۶۶-۱ آلفا-۲)", + "fi": "valtion koodi (ISO 3166-1 alpha-2)", + "fr": "code ISO 3166-1 alpha-2 du pays", + "gl": "código ISO 3166-1 de dúas letras", + "gu": "ISO 3166-1 alpha-2", + "he": "קוד מדינה לפי ISO 3166-1 alpha 2", + "hi": "आईएसओ 3166-1 अल्फा -2", + "hu": "országkód (ISO 3166-1 alpha-2)", + "hy": "ԻՍՕ 3166-1 երկտառ", + "ia": "ISO 3166-1 alpha-2", + "id": "kode negara (ISO 3166-1 alpha-2)", + "ilo": "kodigo ti pagilian IATA", + "is": "ISO 3166-1 alpha-2 landskóði", + "it": "ISO 3166-1 alpha-2", + "ja": "国名コード (ISO 3166-1 alpha-2)", + "ka": "ქვეყნის კოდი (ISO 3166-1 alpha-2)", + "ko": "ISO 3166-1 코드 (alpha-2)", + "la": "siglum civitatis (ISO 3166-1 alpha-2)", + "lb": "Lännercode (ISO 3166-1 alpha-2)", + "lv": "valsts kods (ISO 3166-1 alpha-2)", + "mg": "ISO 3166-1 alpha-2", + "min": "kode nagara (ISO 3166-1 alpha-2)", + "mk": "ISO 3166-1 алфа-2", + "mr": "आय.एस.ओ. ३१६६-१ कोड", + "ms": "kod negara (ISO 3166-1 alpha-2)", + "mt": "kodiċi ISO 3166-1 alfa-2", + "nb": "landskode (ISO 3166-1 alfa-2)", + "nds": "Lännerkood (ISO 3166-1 alpha-2)", + "nl": "landcode (ISO 3166-1 alpha-2)", + "nn": "landkode (ISO 3166-1 alfa-2)", + "oc": "ISO 3166-1 alpha-2 del país", + "pl": "kod ISO 3166-1 alpha-2", + "pt": "ISO 3166-1 alfa-2", + "pt-br": "ISO 3166-1 alfa-2", + "ro": "cod de țară (ISO 3166-1 alpha-2)", + "ru": "ISO 3166-1 alpha-2", + "scn": "còdici ISO 3166-1 alpha-2", + "sco": "ISO 3166-1 alpha-2", + "sk": "kód krajiny (ISO 3166-1 alpha-2)", + "sl": "dvočrkovna oznaka države (ISO 3166-1 alpha-2)", + "sr": "ISO 3166-1 алфа-2", + "sv": "ISO 3166-1 alpha-2", + "ta": "ஐஎஸ்ஒ 3166-1 ஆல்பா -2", + "te": "ISO 3166-1 అక్షర-2", + "tt": "ике хәрефле ISO 3166-1 коды", + "uk": "код ISO 3166-1 alpha-2", + "uz": "davlat kodi (ISO 3166-1 alpha-2)", + "vi": "mã quốc gia (ISO 3166-1 alpha-2)", + "yi": "לאנד קאד ISO 3166-1 alpha-2", + "zh": "ISO 3166-1 二字母代码", + "zh-cn": "ISO 3166-1 二字母代码", + "zh-hans": "ISO 3166-1 二字母代码", + "zh-hant": "ISO 3166-1 二字母代碼", + "zh-hk": "ISO 3166-1 二字母代碼", + "zh-mo": "ISO 3166-1 二字母代碼", + "zh-sg": "ISO 3166-1 二字母代码", + "zh-tw": "ISO 3166-1 二字母代碼" + } + }, + { + "type": "id", + "id": "iso3", + "label": { + "en": "ISO 3166-1 alpha-3 code", + "af": "landkode (ISO 3166-1 alpha-3)", + "ar": "أيزو 3166-1 حرفي-3", + "be": "код краіны (ISO 3166-1 alpha-3)", + "be-tarask": "код краіны (ISO 3166-1 alpha-3)", + "bn": "রাষ্ট্রীয় কোড (আইএসও ৩১৬৬-১ আলফা-৩)", + "br": "kod ar vro (ISO 3166-1 alpha-3)", + "bs": "ISO 3166-1 alpha-3 kod", + "ca": "codi de país (ISO 3166-1 alpha-3)", + "ckb": "کۆدی وڵات (ISO 3166-1 alpha-3)", + "co": "còdice di paese ISO 3166-1 alpha-2", + "cs": "kód země (ISO 3166-1 alpha-3)", + "da": "landekoder (ISO 3166-1 alfa-3)", + "de": "Ländercode (ISO 3166-1 alpha-3)", + "el": "ISO 3166-1 alpha-3", + "en-gb": "country code (ISO 3166-1 alpha-3)", + "eo": "landokodo (ISO 3166-1 alfa-3)", + "es": "código ISO 3166-1 de tres letras", + "et": "maakood (ISO 3166-1 alpha-3)", + "eu": "ISO 3166-1 alpha-3", + "fa": "کد کشور (ایزو ۳۱۶۶-۱ آلفا-۳)", + "fi": "valtion koodi (ISO 3166-1 alpha-3)", + "fr": "code ISO 3166-1 alpha-3", + "gl": "código ISO 3166-1 de tres letras", + "gu": "ISO 3166-1 alpha-3", + "he": "קוד מדינה לפי ISO 3166-1 alpha 3", + "hi": "आईएसओ 3166-1 अल्फा -3", + "hu": "országkód (ISO 3166-1 alpha-3)", + "ia": "ISO 3166-1 alpha-3", + "id": "kode negara (ISO 3166-1 alpha-3)", + "ilo": "kodigo ti pagilian (ISO 3166-1 alpha-3)", + "is": "ISO 3166-1 alpha-3 landskóði", + "it": "ISO 3166-1 alpha-3", + "ja": "国名コード (ISO 3166-1 alpha-3)", + "ka": "ქვეყნის კოდი (ISO 3166-1 alpha-3)", + "ko": "ISO 3166-1 코드 (alpha-3)", + "la": "siglum civitatis (ISO 3166-1 alpha-3)", + "lb": "Lännercode (ISO 3166-1 alpha-3)", + "lv": "valsts kods (ISO 3166-1 alpha-3)", + "mg": "ISO 3166-1 alpha-3", + "min": "kode nagara (ISO 3166-1 alpha-3)", + "mk": "ISO 3166-1 алфа-3", + "ms": "kod negara (ISO 3166-1 alpha-3)", + "mt": "kodiċi ISO 3166-1 alfa-3", + "nb": "landskode (ISO 3166-1 alfa-3)", + "nds": "Lännerkood (ISO 3166-1 alpha-3)", + "nl": "landcode (ISO 3166-1 alpha-3)", + "nn": "landkode (ISO 3166-1 alfa-3)", + "oc": "ISO 3166-1 alpha-3", + "pl": "kod ISO 3166-1 alpha-3", + "pt": "ISO 3166-1 alfa-3", + "pt-br": "ISO 3166-1 alfa-3", + "ro": "cod de țară (ISO 3166-1 alpha-3)", + "ru": "ISO 3166-1 alpha-3", + "scn": "còdici ISO 3166-1 alpha-3", + "sco": "ISO 3166-1 alpha-3", + "sk": "kód krajiny (ISO 3166-1 alpha-3)", + "sl": "tričkrovna oznaka države (ISO 3166-1 alpha-3)", + "sr": "ISO 3166-1 алфа-3", + "sv": "ISO 3166-1 alpha-3", + "te": "ISO 3166-1 అక్షర-3", + "tt": "өч хәрефле ISO 3166-1 коды", + "uk": "код ISO 3166-1 alpha-3", + "uz": "davlat kodi (ISO 3166-1 alpha-3)", + "vi": "mã quốc gia (ISO 3166-1 alpha-3)", + "yi": "לאנד קאד לויט ISO 3166-1 alpha 3", + "zh": "ISO 3166-1三字母代码", + "zh-cn": "ISO 3166-1三字母代码", + "zh-hans": "ISO 3166-1三字母代码", + "zh-hant": "ISO 3166-1三字母代碼", + "zh-hk": "ISO 3166-1三字母代碼", + "zh-mo": "ISO 3166-1三字母代碼", + "zh-sg": "ISO 3166-1三字母代码", + "zh-tw": "ISO 3166-1三字母代碼" + } + }, + { + "type": "property", + "id": "name", + "label": { + "en": "name", + "am": "ስም", + "ar": "الاسم", + "ast": "alcuñu", + "ba": "исем", + "be-tarask": "імя", + "bn": "নাম", + "ca": "nom", + "cs": "jméno", + "cy": "enw", + "da": "navn", + "de": "Name", + "el": "όνομα", + "eo": "nomo", + "es": "nombre", + "et": "nimi", + "eu": "izena", + "fi": "nimi", + "fo": "navn", + "fr": "nom", + "ga": "ainm", + "gl": "nome", + "he": "שם", + "hi": "नाम", + "hsb": "mjeno", + "hu": "név", + "id": "nama", + "it": "nome", + "ja": "名前", + "ko": "성명", + "ku-latn": "nav", + "lb": "Numm", + "lfn": "nom", + "mk": "име", + "mr": "नाव", + "ms": "nama", + "nb": "navn", + "ne": "नाम", + "nl": "naam", + "nn": "namn", + "oc": "nom", + "or": "ନାମ", + "pam": "lagyu", + "pl": "nazwa", + "pt": "nome", + "ro": "nume", + "ru": "имя", + "sq": "name", + "sv": "namn", + "sw": "jina", + "te": "పేరు", + "tg": "ном", + "tl": "pangalan", + "tr": "isim", + "uk": "відомий під іменем", + "ur": "نام", + "vi": "tên", + "yo": "orúkọ", + "zh": "名称", + "zh-hans": "名称" + } + } + ], + "legacy_ids": [ + "world_countries" + ], + "layer_name": { + "aeb-arab": "البلاد", + "af": "land", + "an": "país", + "ar": "البلد", + "ast": "país", + "ba": "дәүләт", + "bar": "Stoot", + "be": "краіна", + "be-tarask": "краіна", + "bg": "държава", + "bn": "রাষ্ট্র", + "br": "Stad", + "bs": "država", + "ca": "estat", + "ce": "пачхьалкх", + "ckb": "وڵات", + "co": "paese", + "cs": "stát`", + "cy": "gwladwriaeth", + "da": "land", + "de": "Staat", + "de-at": "Staat", + "de-ch": "Land", + "el": "χώρα", + "en": "World Countries", + "en-ca": "World Countries", + "en-gb": "World Countries", + "eo": "lando", + "es": "país", + "et": "riik", + "eu": "herrialdea", + "fa": "کشور", + "fi": "valtio", + "fo": "land", + "fr": "pays", + "frr": "Stoot", + "fy": "lân", + "ga": "tír", + "gd": "dùthaich", + "gl": "país", + "gsw": "Staat", + "gu": "દેશ", + "ha": "ƙasa", + "he": "מדינה", + "hi": "देश", + "hr": "zemlja", + "hsb": "stat", + "hu": "ország", + "hy": "երկիր", + "ia": "pais", + "id": "negara", + "ilo": "pagilian", + "io": "lando", + "is": "land", + "it": "Paese", + "ja": "国", + "jbo": "gugde", + "ka": "ქვეყანა", + "kn": "ದೇಶ", + "ko": "다음 나라의 것임", + "ksh": "Schtaht", + "ku-latn": "welat", + "ky": "өлкөсү", + "la": "civitas", + "lb": "Land", + "lfn": "pais", + "lmo": "Stàt", + "lt": "valstybė", + "lv": "valsts", + "mai": "देश", + "mg": "Firenena", + "mhr": "мланде", + "min": "nagara", + "mk": "земја", + "ml": "രാജ്യം", + "mr": "देश", + "ms": "negara", + "mt": "pajjiż", + "my": "နိုင်ငံ", + "myv": "мастор", + "nb": "land", + "nds": "Land", + "nds-nl": "laand", + "ne": "देश", + "nl": "land", + "nn": "land", + "nso": "Naga", + "oc": "país", + "olo": "Mua", + "or": "ଦେଶ", + "pa": "ਦੇਸ਼", + "pam": "bangsa", + "pcd": "pays", + "pl": "państwo", + "ps": "هېواد", + "pt": "país", + "pt-br": "país", + "rm": "stadi", + "ro": "țară", + "roa-tara": "State", + "ru": "государство", + "scn": "paìsi", + "sco": "kintra", + "se": "riika", + "sh": "zemlja", + "si": "රට", + "sk": "štát", + "sl": "država", + "sq": "shteti", + "sr": "држава", + "sr-ec": "држава", + "sr-el": "država", + "stq": "Lound", + "sv": "land", + "ta": "நாடு", + "te": "దేశం", + "tg": "мамлакат", + "tg-cyrl": "давлат", + "th": "ประเทศ", + "tl": "bansa", + "tr": "ülkesi", + "ts": "Tiko", + "tt": "дәүләт", + "tt-cyrl": "дәүләт", + "tt-latn": "däwlät", + "uk": "держава", + "ur": "ملک", + "uz": "davlat", + "vi": "quốc gia", + "vo": "län", + "yi": "מדינה", + "yo": "orílè-èdè", + "yue": "國", + "zh": "国家", + "zh-cn": "国家", + "zh-hans": "国家", + "zh-hant": "國家", + "zh-hk": "國家", + "zh-mo": "國家", + "zh-my": "国家", + "zh-sg": "国家", + "zh-tw": "國家" + } + } + ]} diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest_proxied.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest_proxied.json new file mode 100644 index 000000000000..f73e5ea3a587 --- /dev/null +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest_proxied.json @@ -0,0 +1,16 @@ +{ + "services": [ + { + "id": "tiles_v2", + "name": "Elastic Maps Tile Service", + "manifest": "/tiles", + "type": "tms" + }, + { + "id": "geo_layers", + "name": "Elastic Maps Vector Service", + "manifest": "/files", + "type": "file" + } + ] +} diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_proxied.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_proxied.json new file mode 100644 index 000000000000..befbcf1b96c1 --- /dev/null +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_proxied.json @@ -0,0 +1,12 @@ +{ + "tilejson": "2.0.0", + "name": "OSM Bright", + "attribution": "
    © MapTiler © OpenStreetMap contributors", + "minzoom": 0, + "maxzoom": 10, + "bounds": [-180, -85.0511, 180, 85.0511], + "format": "png", + "type": "baselayer", + "tiles": ["/tiles/raster/osm_bright/{x}/{y}/{z}/.jpg"], + "center": [0, 0, 2] +} diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles_proxied.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles_proxied.json new file mode 100644 index 000000000000..16166d83a227 --- /dev/null +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles_proxied.json @@ -0,0 +1,32 @@ +{ + "services": [ + { + "id": "road_map", + "name": { "en": "Road Map - Bright" }, + "attribution": [ + { + "label": { "en": "OpenStreetMap contributors" }, + "url": { "en": "https://www.openstreetmap.org/copyright" } + }, + { "label": { "en": "OpenMapTiles" }, "url": { "en": "https://openmaptiles.org" } }, + { "label": { "en": "MapTiler" }, "url": { "en": "https://www.maptiler.com" } }, + { + "label": { "en": "Elastic Maps Service" }, + "url": { "en": "https://www.elastic.co/elastic-maps-service" } + } + ], + "formats": [ + { + "locale": "en", + "format": "vector", + "url": "/vector/osm.bright.json" + }, + { + "locale": "en", + "format": "raster", + "url": "/tiles/raster/osm.bright.json" + } + ] + } + ] +} diff --git a/src/legacy/ui/public/vis/agg_configs.d.ts b/src/legacy/ui/public/vis/agg_configs.d.ts index c9557aef4da0..d6223712a744 100644 --- a/src/legacy/ui/public/vis/agg_configs.d.ts +++ b/src/legacy/ui/public/vis/agg_configs.d.ts @@ -17,4 +17,11 @@ * under the License. */ -export type AggConfigs = any; +import { IndexedArray } from '../indexed_array'; +import { AggConfig } from './agg_config'; + +export interface AggConfigs extends IndexedArray { + bySchemaGroup: { + [key: string]: AggConfig[]; + }; +} diff --git a/src/legacy/ui/public/vis/draggable/__tests__/draggable.js b/src/legacy/ui/public/vis/draggable/__tests__/draggable.js deleted file mode 100644 index fc0601a9b67e..000000000000 --- a/src/legacy/ui/public/vis/draggable/__tests__/draggable.js +++ /dev/null @@ -1,139 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -let init; -let $rootScope; -let $compile; - -describe(`draggable_* directives`, function () { - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function ($injector) { - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - init = function init(markup = '') { - const $parentScope = $rootScope.$new(); - $parentScope.items = [ - { name: 'item_1' }, - { name: 'item_2' }, - { name: 'item_3' } - ]; - - // create the markup - const $elem = angular.element(`
    `); - $elem.html(markup); - - // compile the directive - $compile($elem)($parentScope); - $parentScope.$apply(); - - const $scope = $elem.scope(); - - return { $parentScope, $scope, $elem }; - }; - })); - - describe(`draggable_container directive`, function () { - it(`should expose the drake`, function () { - const { $scope } = init(); - expect($scope.drake).to.be.an(Object); - }); - - it(`should expose the controller`, function () { - const { $scope } = init(); - expect($scope.draggableContainerCtrl).to.be.an(Object); - }); - - it(`should pull item list from directive attribute`, function () { - const { $scope, $parentScope } = init(); - expect($scope.draggableContainerCtrl.getList()).to.eql($parentScope.items); - }); - - it(`should not be able to move extraneous DOM elements`, function () { - const bare = angular.element(`
    `); - const { $scope } = init(); - expect($scope.drake.canMove(bare[0])).to.eql(false); - }); - - it(`should not be able to move non-[draggable-item] elements`, function () { - const bare = angular.element(`
    `); - const { $scope, $elem } = init(); - $elem.append(bare); - expect($scope.drake.canMove(bare[0])).to.eql(false); - }); - - it(`shouldn't be able to move extraneous [draggable-item] elements`, function () { - const anotherParent = angular.element(`
    `); - const item = angular.element(`
    `); - const scope = $rootScope.$new(); - anotherParent.append(item); - $compile(anotherParent)(scope); - $compile(item)(scope); - scope.$apply(); - const { $scope } = init(); - expect($scope.drake.canMove(item[0])).to.eql(false); - }); - - it(`shouldn't be able to move [draggable-item] if it has a handle`, function () { - const { $scope, $elem } = init(` -
    -
    -
    - `); - const item = $elem.find(`[draggable-item]`); - expect($scope.drake.canMove(item[0])).to.eql(false); - }); - - it(`should be able to move [draggable-item] by its handle`, function () { - const { $scope, $elem } = init(` -
    -
    -
    - `); - const handle = $elem.find(`[draggable-handle]`); - expect($scope.drake.canMove(handle[0])).to.eql(true); - }); - }); - - describe(`draggable_item`, function () { - it(`should be required to be a child to [draggable-container]`, function () { - const item = angular.element(`
    `); - const scope = $rootScope.$new(); - expect(() => { - $compile(item)(scope); - scope.$apply(); - }).to.throwException(/controller(.+)draggableContainer(.+)required/i); - }); - }); - - describe(`draggable_handle`, function () { - it('should be required to be a child to [draggable-item]', function () { - const handle = angular.element(`
    `); - const scope = $rootScope.$new(); - expect(() => { - $compile(handle)(scope); - scope.$apply(); - }).to.throwException(/controller(.+)draggableItem(.+)required/i); - }); - }); -}); diff --git a/src/legacy/ui/public/vis/draggable/draggable_container.js b/src/legacy/ui/public/vis/draggable/draggable_container.js deleted file mode 100644 index d1e11fc9740b..000000000000 --- a/src/legacy/ui/public/vis/draggable/draggable_container.js +++ /dev/null @@ -1,133 +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 dragula from 'dragula'; -import 'dragula/dist/dragula.css'; -import { uiModules } from '../../modules'; -import { move } from '../../utils/collection'; - -uiModules - .get('kibana') - .directive('draggableContainer', function () { - - const $scopes = new WeakMap(); - - return { - restrict: 'A', - scope: true, - controllerAs: 'draggableContainerCtrl', - controller($scope, $attrs, $parse, $element) { - $scopes.set($element.get(0), $scope); - this.linkDraggableItem = (el, $scope) => { - $scopes.set(el, $scope); - }; - - this.getList = () => $parse($attrs.draggableContainer)($scope); - }, - link($scope, $el) { - const drake = dragula({ - containers: $el.toArray(), - moves(el, source, handle) { - const itemScope = $scopes.get(el); - if (!itemScope || !('draggableItemCtrl' in itemScope)) { - return; // only [draggable-item] is draggable - } - return itemScope.draggableItemCtrl.moves(handle); - } - }); - - const drakeEvents = [ - 'cancel', - 'cloned', - 'drag', - 'dragend', - 'drop', - 'out', - 'over', - 'remove', - 'shadow' - ]; - const prettifiedDrakeEvents = { - drag: 'start', - dragend: 'end' - }; - - drakeEvents.forEach(type => { - drake.on(type, (el, ...args) => forwardEvent(type, el, ...args)); - }); - drake.on('drag', markDragging(true)); - drake.on('dragend', markDragging(false)); - drake.on('drop', drop); - $scope.$on('$destroy', drake.destroy); - $scope.drake = drake; - - function markDragging(isDragging) { - return el => { - const scope = $scopes.get(el); - if (!scope) return; - scope.isDragging = isDragging; - scope.$apply(); - }; - } - - function forwardEvent(type, el, ...args) { - const name = `drag-${prettifiedDrakeEvents[type] || type}`; - const scope = $scopes.get(el); - if (!scope) return; - scope.$broadcast(name, el, ...args); - } - - function drop(el, target, source, sibling) { - const list = $scope.draggableContainerCtrl.getList(); - const itemScope = $scopes.get(el); - if (!itemScope) return; - const item = itemScope.draggableItemCtrl.getItem(); - const fromIndex = list.indexOf(item); - const siblingIndex = getItemIndexFromElement(list, sibling); - - const toIndex = getTargetIndex(list, fromIndex, siblingIndex); - move(list, item, toIndex); - } - - function getTargetIndex(list, fromIndex, siblingIndex) { - if (siblingIndex === -1) { - // means the item was dropped at the end of the list - return list.length - 1; - } else if (fromIndex < siblingIndex) { - // An item moving from a lower index to a higher index will offset the - // index of the earlier items by one. - return siblingIndex - 1; - } - return siblingIndex; - } - - function getItemIndexFromElement(list, element) { - if (!element) return -1; - - const scope = $scopes.get(element); - if (!scope) return; - const item = scope.draggableItemCtrl.getItem(); - const index = list.indexOf(item); - - return index; - } - } - }; - - }); diff --git a/src/legacy/ui/public/vis/draggable/draggable_handle.js b/src/legacy/ui/public/vis/draggable/draggable_handle.js deleted file mode 100644 index ce8815c2a749..000000000000 --- a/src/legacy/ui/public/vis/draggable/draggable_handle.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 { uiModules } from '../../modules'; - -uiModules - .get('kibana') - .directive('draggableHandle', function () { - return { - restrict: 'A', - require: '^draggableItem', - link($scope, $el, attr, ctrl) { - ctrl.registerHandle($el); - $el.addClass('gu-handle'); - } - }; - }); diff --git a/src/legacy/ui/public/vis/draggable/draggable_item.js b/src/legacy/ui/public/vis/draggable/draggable_item.js deleted file mode 100644 index 943438aa61b5..000000000000 --- a/src/legacy/ui/public/vis/draggable/draggable_item.js +++ /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 $ from 'jquery'; -import { uiModules } from '../../modules'; - -uiModules - .get('kibana') - .directive('draggableItem', function () { - return { - restrict: 'A', - require: '^draggableContainer', - scope: true, - controllerAs: 'draggableItemCtrl', - controller($scope, $attrs, $parse) { - const dragHandles = $(); - - this.getItem = () => $parse($attrs.draggableItem)($scope); - this.registerHandle = $el => { - dragHandles.push(...$el); - }; - this.moves = handle => { - const $handle = $(handle); - const $anywhereInParentChain = $handle.parents().addBack(); - const movable = dragHandles.is($anywhereInParentChain); - return movable; - }; - }, - link($scope, $el, attr, draggableController) { - draggableController.linkDraggableItem($el.get(0), $scope); - } - }; - }); diff --git a/src/legacy/ui/public/vis/editors/_index.scss b/src/legacy/ui/public/vis/editors/_index.scss index da6e0bd8837e..1c4169bcf371 100644 --- a/src/legacy/ui/public/vis/editors/_index.scss +++ b/src/legacy/ui/public/vis/editors/_index.scss @@ -1,2 +1 @@ -@import './components/index'; @import './default/index'; diff --git a/src/legacy/ui/public/vis/editors/components/__snapshots__/editor_options_group.test.js.snap b/src/legacy/ui/public/vis/editors/components/__snapshots__/editor_options_group.test.js.snap deleted file mode 100644 index 7253b47841a6..000000000000 --- a/src/legacy/ui/public/vis/editors/components/__snapshots__/editor_options_group.test.js.snap +++ /dev/null @@ -1,200 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders as expected 1`] = ` -
    -
    -
    - -
    -
    -
    -
    -
    - - Children - - - within the editor group - -
    -
    -
    -
    -
    -`; - -exports[` renders as expected with actions 1`] = ` -
    -
    -
    - -
    - -
    -
    -
    -
    -
    -
    - - Some children - -
    -
    -
    -
    -
    -`; - -exports[` renders as expected with initial collapsed 1`] = ` -
    -
    -
    - -
    -
    -
    -
    -
    - - Children - - - within the editor group - -
    -
    -
    -
    -
    -`; diff --git a/src/legacy/ui/public/vis/editors/components/_editor_options_group.scss b/src/legacy/ui/public/vis/editors/components/_editor_options_group.scss deleted file mode 100644 index 23164a216278..000000000000 --- a/src/legacy/ui/public/vis/editors/components/_editor_options_group.scss +++ /dev/null @@ -1,3 +0,0 @@ -.visEditorOptionsGroup__panel + .visEditorOptionsGroup__panel { - margin-top: $euiSizeS; -} diff --git a/src/legacy/ui/public/vis/editors/components/_index.scss b/src/legacy/ui/public/vis/editors/components/_index.scss deleted file mode 100644 index af09ec30b381..000000000000 --- a/src/legacy/ui/public/vis/editors/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './editor_options_group'; diff --git a/src/legacy/ui/public/vis/editors/components/editor_options_group.js b/src/legacy/ui/public/vis/editors/components/editor_options_group.js deleted file mode 100644 index 6f9fe3ff02ed..000000000000 --- a/src/legacy/ui/public/vis/editors/components/editor_options_group.js +++ /dev/null @@ -1,79 +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 PropTypes from 'prop-types'; - -import { - EuiAccordion, - EuiPanel, - EuiSpacer, - EuiTitle, - htmlIdGenerator, -} from '@elastic/eui'; - - -/** - * A component to group different options in an editor together and give them - * a title. Should be used for all visualize editors when grouping options, - * to produce an aligned look and feel. - */ -function EditorOptionsGroup(props) { - return ( - - -

    {props.title}

    - - } - > - - { props.children } -
    -
    - ); -} - -EditorOptionsGroup.propTypes = { - /** - * The title of this options group, which will be shown with the group. - */ - title: PropTypes.string.isRequired, - /** - * Add additional elements as actions to the group. - */ - actions: PropTypes.node, - /** - * Whether the panel should be collapsed by default. - */ - initialIsCollapsed: PropTypes.bool, - /** - * All elements that should be within this group. - */ - children: PropTypes.node.isRequired, -}; - -export { EditorOptionsGroup }; diff --git a/src/legacy/ui/public/vis/editors/components/editor_options_group.test.js b/src/legacy/ui/public/vis/editors/components/editor_options_group.test.js deleted file mode 100644 index 2f1ee6a303f8..000000000000 --- a/src/legacy/ui/public/vis/editors/components/editor_options_group.test.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 { render } from 'enzyme'; - -import 'test_utils/static_html_id_generator'; - -import { EuiButtonIcon } from '@elastic/eui'; -import { EditorOptionsGroup } from './editor_options_group'; - -describe('', () => { - it('renders as expected', () => { - const group = render( - - Children - within the editor group - - ); - expect(group).toMatchSnapshot(); - }); - - it('renders as expected with actions', () => { - const group = render( - - } - > - Some children - - ); - expect(group).toMatchSnapshot(); - }); - - it('renders as expected with initial collapsed', () => { - const group = render( - - Children - within the editor group - - ); - expect(group).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/components/index.js b/src/legacy/ui/public/vis/editors/components/index.js deleted file mode 100644 index 23e1cfbd2767..000000000000 --- a/src/legacy/ui/public/vis/editors/components/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 { EditorOptionsGroup } from './editor_options_group'; diff --git a/src/legacy/ui/public/vis/editors/config/editor_config_providers.test.ts b/src/legacy/ui/public/vis/editors/config/editor_config_providers.test.ts index 187772f859e1..d9234f23cd45 100644 --- a/src/legacy/ui/public/vis/editors/config/editor_config_providers.test.ts +++ b/src/legacy/ui/public/vis/editors/config/editor_config_providers.test.ts @@ -35,7 +35,7 @@ describe('EditorConfigProvider', () => { searchable: true, }, ], - }; + } as any; beforeEach(() => { registry = new EditorConfigProviderRegistry(); diff --git a/src/legacy/ui/public/vis/editors/default/__tests__/agg.js b/src/legacy/ui/public/vis/editors/default/__tests__/agg.js deleted file mode 100644 index 5ca17a7c9b96..000000000000 --- a/src/legacy/ui/public/vis/editors/default/__tests__/agg.js +++ /dev/null @@ -1,101 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../agg'; - - -describe('Vis-Editor-Agg plugin directive', function () { - const $parentScope = {}; - let $elem; - - function makeConfig(which) { - const schemaMap = { - radius: { - title: 'Dot Size', - min: 0, - max: 1 - }, - metric: { - title: 'Y-Axis', - min: 1, - max: Infinity - } - }; - const typeOptions = ['count', 'avg', 'sum', 'min', 'max', 'cardinality']; - which = which || 'metric'; - - const schema = schemaMap[which]; - - return { - min: schema.min, - max: schema.max, - name: which, - title: schema.title, - group: 'metrics', - aggFilter: typeOptions, - // AggParams object - params: [] - }; - } - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function ($rootScope, $compile) { - $parentScope.agg = { - id: 1, - params: {}, - schema: makeConfig() - }; - $parentScope.groupName = 'metrics'; - $parentScope.group = [{ - id: '1', - schema: makeConfig() - }, { - id: '2', - schema: makeConfig('radius') - }]; - - // share the scope - _.defaults($parentScope, $rootScope, Object.getPrototypeOf($rootScope)); - - // make the element - $elem = angular.element( - '
    ' - ); - - // compile the html - $compile($elem)($parentScope); - - // Digest everything - $elem.scope().$digest(); - })); - - it('should only add the close button if there is more than the minimum', function () { - expect($parentScope.canRemove($parentScope.agg)).to.be(false); - $parentScope.group.push({ - id: '3', - schema: makeConfig() - }); - expect($parentScope.canRemove($parentScope.agg)).to.be(true); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/__tests__/default_editor_utils.test.tsx b/src/legacy/ui/public/vis/editors/default/__tests__/default_editor_utils.test.tsx deleted file mode 100644 index a028ea9701e4..000000000000 --- a/src/legacy/ui/public/vis/editors/default/__tests__/default_editor_utils.test.tsx +++ /dev/null @@ -1,195 +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 { groupAggregationsBy } from '../default_editor_utils'; -import { AggGroupNames } from '../agg_groups'; - -const aggs = [ - { - title: 'Count', - type: AggGroupNames.Metrics, - subtype: 'Metric Aggregations', - }, - { - title: 'Average', - type: AggGroupNames.Metrics, - subtype: 'Metric Aggregations', - }, - { - title: 'Cumulative Sum', - type: AggGroupNames.Metrics, - subtype: 'Parent Pipeline Aggregations', - }, - { - title: 'Min Bucket', - type: AggGroupNames.Metrics, - subtype: 'Parent Pipeline Aggregations', - }, - { - title: 'Sub string agg', - type: 'string', - subtype: 'Sub-String aggregations', - }, - { - title: 'String agg', - type: 'string', - subtype: 'String aggregations', - }, -]; - -describe('Default Editor groupAggregationsBy', () => { - it('should return aggs grouped by default type field', () => { - const groupedAggs = [ - { - label: AggGroupNames.Metrics, - options: [ - { - label: 'Average', - value: { - title: 'Average', - type: AggGroupNames.Metrics, - subtype: 'Metric Aggregations', - }, - }, - { - label: 'Count', - value: { - title: 'Count', - type: AggGroupNames.Metrics, - subtype: 'Metric Aggregations', - }, - }, - { - label: 'Cumulative Sum', - value: { - title: 'Cumulative Sum', - type: AggGroupNames.Metrics, - subtype: 'Parent Pipeline Aggregations', - }, - }, - - { - label: 'Min Bucket', - value: { - title: 'Min Bucket', - type: AggGroupNames.Metrics, - subtype: 'Parent Pipeline Aggregations', - }, - }, - ], - }, - { - label: 'string', - options: [ - { - label: 'String agg', - value: { - title: 'String agg', - type: 'string', - subtype: 'String aggregations', - }, - }, - { - label: 'Sub string agg', - value: { - title: 'Sub string agg', - type: 'string', - subtype: 'Sub-String aggregations', - }, - }, - ], - }, - ]; - expect(groupAggregationsBy(aggs)).toEqual(groupedAggs); - }); - it('should return aggs grouped by subtype field', () => { - const groupedAggs = [ - { - label: 'Metric Aggregations', - options: [ - { - label: 'Average', - value: { - title: 'Average', - type: AggGroupNames.Metrics, - subtype: 'Metric Aggregations', - }, - }, - { - label: 'Count', - value: { - title: 'Count', - type: AggGroupNames.Metrics, - subtype: 'Metric Aggregations', - }, - }, - ], - }, - { - label: 'Parent Pipeline Aggregations', - options: [ - { - label: 'Cumulative Sum', - value: { - title: 'Cumulative Sum', - type: AggGroupNames.Metrics, - subtype: 'Parent Pipeline Aggregations', - }, - }, - - { - label: 'Min Bucket', - value: { - title: 'Min Bucket', - type: AggGroupNames.Metrics, - subtype: 'Parent Pipeline Aggregations', - }, - }, - ], - }, - { - label: 'String aggregations', - options: [ - { - label: 'String agg', - value: { - title: 'String agg', - type: 'string', - subtype: 'String aggregations', - }, - }, - ], - }, - { - label: 'Sub-String aggregations', - options: [ - { - label: 'Sub string agg', - value: { - title: 'Sub string agg', - type: 'string', - subtype: 'Sub-String aggregations', - }, - }, - ], - }, - ]; - expect(groupAggregationsBy(aggs, 'subtype')).toEqual(groupedAggs); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/__tests__/keyboard_move.js b/src/legacy/ui/public/vis/editors/default/__tests__/keyboard_move.js deleted file mode 100644 index b24e2918ac07..000000000000 --- a/src/legacy/ui/public/vis/editors/default/__tests__/keyboard_move.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 angular from 'angular'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; -import { Direction } from '../keyboard_move'; -import { keyCodes } from '@elastic/eui'; - -describe('keyboardMove directive', () => { - - let $compile; - let $rootScope; - - function createTestButton(callback) { - const scope = $rootScope.$new(); - scope.callback = callback; - return $compile('')(scope); - } - - function createKeydownEvent(keyCode) { - const e = angular.element.Event('keydown'); // eslint-disable-line new-cap - e.which = keyCode; - return e; - } - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject((_$rootScope_, _$compile_) => { - $compile = _$compile_; - $rootScope = _$rootScope_; - })); - - it('should call the callback when pressing up', () => { - const spy = sinon.spy(); - const button = createTestButton(spy); - button.trigger(createKeydownEvent(keyCodes.UP)); - expect(spy.calledWith(Direction.up)).to.be(true); - }); - - it('should call the callback when pressing down', () => { - const spy = sinon.spy(); - const button = createTestButton(spy); - button.trigger(createKeydownEvent(keyCodes.DOWN)); - expect(spy.calledWith(Direction.down)).to.be(true); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/_agg.scss b/src/legacy/ui/public/vis/editors/default/_agg.scss index e5bcc31c5b53..b2d131053868 100644 --- a/src/legacy/ui/public/vis/editors/default/_agg.scss +++ b/src/legacy/ui/public/vis/editors/default/_agg.scss @@ -4,7 +4,7 @@ .visEditorAgg__rangesTable { td { - padding: 0 $vis-editor-agg-editor-spacing $vis-editor-agg-editor-spacing 0; + padding: 0 $euiSizeS $euiSizeS 0; &:last-child { padding-right: 0; @@ -12,36 +12,10 @@ } } -.visEditorAgg__formRow--flex { - display: flex; - - > * { - flex: 1 1 auto; - margin-right: $vis-editor-agg-editor-spacing; - - &:last-child { - margin-right: 0px; - } - } -} - -/** - * 1. Hack to split child elements evenly. - */ -.visEditorAgg__formRow--split { - flex: 1 1 0 !important; /* 1 */ -} - -.visEditorAgg__sliderValue { - @include euiFontSize; - align-self: center; - margin: 0 0 0 $vis-editor-agg-editor-spacing; -} - .visEditorAgg__subAgg { border: $euiBorderThick; border-radius: $euiBorderRadius; background-color: transparent; - margin: $vis-editor-agg-editor-spacing 0; - padding: $vis-editor-agg-editor-spacing; + margin: $euiSizeS 0; + padding: $euiSizeS; } diff --git a/src/legacy/ui/public/vis/editors/default/_agg_select.scss b/src/legacy/ui/public/vis/editors/default/_agg_select.scss deleted file mode 100644 index 0ecbccade604..000000000000 --- a/src/legacy/ui/public/vis/editors/default/_agg_select.scss +++ /dev/null @@ -1,7 +0,0 @@ -.visEditorAggSelect__helpLink { - @include euiFontSizeXS; -} - -.visEditorAggSelect__formRow { - margin-bottom: $euiSizeS; -} diff --git a/src/legacy/ui/public/vis/editors/default/_index.scss b/src/legacy/ui/public/vis/editors/default/_index.scss index 3f04e89846f4..d5938a0298d3 100644 --- a/src/legacy/ui/public/vis/editors/default/_index.scss +++ b/src/legacy/ui/public/vis/editors/default/_index.scss @@ -1,6 +1,5 @@ $vis-editor-sidebar-basis: (100/12) * 2%; // two of twelve columns $vis-editor-sidebar-min-width: 350px; -$vis-editor-agg-editor-spacing: $euiSizeS; $vis-editor-resizer-width: $euiSizeM; // Main layout @@ -10,4 +9,3 @@ $vis-editor-resizer-width: $euiSizeM; // Components @import './agg'; @import './agg_params'; -@import './agg_select'; diff --git a/src/legacy/ui/public/vis/editors/default/_sidebar.scss b/src/legacy/ui/public/vis/editors/default/_sidebar.scss index db9c3337beed..4efee6168a0f 100644 --- a/src/legacy/ui/public/vis/editors/default/_sidebar.scss +++ b/src/legacy/ui/public/vis/editors/default/_sidebar.scss @@ -119,7 +119,7 @@ // Collapsible section .visEditorSidebar__collapsible { - background-color: transparentize($euiColorLightShade, .85); + background-color: lightOrDarkTheme($euiPageBackgroundColor, $euiColorLightestShade); } .visEditorSidebar__collapsible--margin { @@ -170,12 +170,6 @@ @include euiTextTruncate; } -.visEditorSidebar__collapsibleTitleDescription--danger { - color: $euiColorDanger; - font-weight: $euiFontWeightBold; -} - - // // FORMS // @@ -225,3 +219,16 @@ margin-top: $euiSizeS; margin-bottom: $euiSizeS; } + +.visEditorSidebar__aggGroupAccordionButtonContent { + font-size: $euiFontSizeS; + + span { + color: $euiColorDarkShade; + } +} + +.visEditorSidebar__switchOptionFormRow { + margin-top: $euiSizeS; + padding-bottom: $euiSizeS; +} diff --git a/src/legacy/ui/public/vis/editors/default/agg.html b/src/legacy/ui/public/vis/editors/default/agg.html deleted file mode 100644 index 4c010d575f19..000000000000 --- a/src/legacy/ui/public/vis/editors/default/agg.html +++ /dev/null @@ -1,143 +0,0 @@ -
    - -
    - - - - - - - {{ describe() }} - - - - - - - -
    - - - - - - - - - - - - -
    - -
    - - - - - - -
    - - - diff --git a/src/legacy/ui/public/vis/editors/default/agg.js b/src/legacy/ui/public/vis/editors/default/agg.js deleted file mode 100644 index f3f835043c86..000000000000 --- a/src/legacy/ui/public/vis/editors/default/agg.js +++ /dev/null @@ -1,188 +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 { i18n } from '@kbn/i18n'; -import './agg_params'; -import './agg_add'; -import './controls/agg_controls'; -import { Direction } from './keyboard_move'; -import _ from 'lodash'; -import './fancy_forms'; -import { uiModules } from '../../../modules'; -import aggTemplate from './agg.html'; -import { move } from '../../../utils/collection'; - -uiModules - .get('app/visualize') - .directive('visEditorAgg', () => { - return { - restrict: 'A', - template: aggTemplate, - require: ['^form', '^ngModel'], - link: function ($scope, $el, attrs, [kbnForm, ngModelCtrl]) { - $scope.editorOpen = !!$scope.agg.brandNew; - $scope.aggIsTooLow = false; - - $scope.$watch('editorOpen', function (open) { - // make sure that all of the form inputs are "touched" - // so that their errors propagate - if (!open) kbnForm.$setTouched(); - }); - - $scope.$watchMulti([ - '$index', - 'group.length' - ], function () { - $scope.aggIsTooLow = calcAggIsTooLow(); - }); - - if ($scope.groupName === 'buckets') { - $scope.$watchMulti([ - '$last', - 'lastParentPipelineAggTitle', - 'agg.type' - ], function ([isLastBucket, lastParentPipelineAggTitle, aggType]) { - $scope.error = null; - $scope.disabledParams = []; - - if (!lastParentPipelineAggTitle || !isLastBucket || !aggType) { - return; - } - - if (['date_histogram', 'histogram'].includes(aggType.name)) { - $scope.onAggParamsChange( - $scope.agg.params, - 'min_doc_count', - // "histogram" agg has an editor for "min_doc_count" param, which accepts boolean - // "date_histogram" agg doesn't have an editor for "min_doc_count" param, it should be set as a numeric value - aggType.name === 'histogram' ? true : 0); - $scope.disabledParams = ['min_doc_count']; - } else { - $scope.error = i18n.translate('common.ui.aggTypes.metrics.wrongLastBucketTypeErrorMessage', { - defaultMessage: 'Last bucket aggregation must be "Date Histogram" or "Histogram" when using "{type}" metric aggregation.', - values: { type: lastParentPipelineAggTitle }, - description: 'Date Histogram and Histogram should not be translated', - }); - } - }); - } - - /** - * Describe the aggregation, for display in the collapsed agg header - * @return {[type]} [description] - */ - $scope.describe = function () { - if (!$scope.agg.type || !$scope.agg.type.makeLabel) return ''; - const label = $scope.agg.type.makeLabel($scope.agg); - return label ? label : ''; - }; - - $scope.$on('drag-start', () => { - $scope.editorWasOpen = $scope.editorOpen; - $scope.editorOpen = false; - $scope.$emit('agg-drag-start', $scope.agg); - }); - - $scope.$on('drag-end', () => { - $scope.editorOpen = $scope.editorWasOpen; - $scope.$emit('agg-drag-end', $scope.agg); - }); - - /** - * Move aggregations down/up in the priority list by pressing arrow keys. - */ - $scope.onPriorityReorder = function (direction) { - const positionOffset = direction === Direction.down ? 1 : -1; - - const currentPosition = $scope.group.indexOf($scope.agg); - const newPosition = Math.max(0, Math.min(currentPosition + positionOffset, $scope.group.length - 1)); - move($scope.group, currentPosition, newPosition); - $scope.$emit('agg-reorder'); - }; - - $scope.remove = function (agg) { - const aggs = $scope.state.aggs; - const index = aggs.indexOf(agg); - - if (index === -1) { - return; - } - - aggs.splice(index, 1); - }; - - $scope.canRemove = function (aggregation) { - const metricCount = _.reduce($scope.group, function (count, agg) { - return (agg.schema.name === aggregation.schema.name) ? ++count : count; - }, 0); - - // make sure the the number of these aggs is above the min - return metricCount > aggregation.schema.min; - }; - - function calcAggIsTooLow() { - if (!$scope.agg.schema.mustBeFirst) { - return false; - } - - const firstDifferentSchema = _.findIndex($scope.group, function (agg) { - return agg.schema !== $scope.agg.schema; - }); - - if (firstDifferentSchema === -1) { - return false; - } - - return $scope.$index > firstDifferentSchema; - } - - // The model can become touched either onBlur event or when the form is submitted. - // We watch $touched to identify when the form is submitted. - $scope.$watch(() => { - return ngModelCtrl.$touched; - }, (value) => { - $scope.formIsTouched = value; - }, true); - - $scope.onAggTypeChange = (agg, value) => { - if (agg.type !== value) { - agg.type = value; - } - }; - - $scope.onAggParamsChange = (params, paramName, value) => { - if (params[paramName] !== value) { - params[paramName] = value; - } - }; - - $scope.setValidity = (isValid) => { - ngModelCtrl.$setValidity(`aggParams${$scope.agg.id}`, isValid); - }; - - $scope.setTouched = (isTouched) => { - if (isTouched) { - ngModelCtrl.$setTouched(); - } else { - ngModelCtrl.$setUntouched(); - } - }; - } - }; - }); diff --git a/src/legacy/ui/public/vis/editors/default/agg_add.js b/src/legacy/ui/public/vis/editors/default/agg_add.js deleted file mode 100644 index a60e9146b01a..000000000000 --- a/src/legacy/ui/public/vis/editors/default/agg_add.js +++ /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. - */ - -import { uiModules } from '../../../modules'; -import { DefaultEditorAggAdd } from './components/default_editor_agg_add'; -import { wrapInI18nContext } from 'ui/i18n'; - -uiModules - .get('kibana') - .directive('visEditorAggAdd', reactDirective => - reactDirective(wrapInI18nContext(DefaultEditorAggAdd), [ - ['group', { watchDepth: 'collection' }], - ['schemas', { watchDepth: 'collection' }], - ['stats', { watchDepth: 'reference' }], - 'groupName', - 'addSchema' - ]) - ); diff --git a/src/legacy/ui/public/vis/editors/default/agg_group.html b/src/legacy/ui/public/vis/editors/default/agg_group.html deleted file mode 100644 index b703d23b4f14..000000000000 --- a/src/legacy/ui/public/vis/editors/default/agg_group.html +++ /dev/null @@ -1,25 +0,0 @@ -
    -
    - {{ groupNameLabel }} -
    - -
    -
    - - -
    -
    -
    - - - -
    -
    - -
    diff --git a/src/legacy/ui/public/vis/editors/default/agg_group.js b/src/legacy/ui/public/vis/editors/default/agg_group.js index 054f4e086b91..5a683454a9c1 100644 --- a/src/legacy/ui/public/vis/editors/default/agg_group.js +++ b/src/legacy/ui/public/vis/editors/default/agg_group.js @@ -17,79 +17,80 @@ * under the License. */ -import _ from 'lodash'; -import './agg'; -import './agg_add'; - +import 'ngreact'; +import { wrapInI18nContext } from 'ui/i18n'; import { uiModules } from '../../../modules'; -import aggGroupTemplate from './agg_group.html'; -import { move } from '../../../utils/collection'; -import { aggGroupNameMaps } from './agg_group_names'; -import { AggConfig } from '../../agg_config'; - -import '../../draggable/draggable_container'; -import '../../draggable/draggable_item'; -import '../../draggable/draggable_handle'; +import { DefaultEditorAggGroup } from './components/agg_group'; uiModules .get('app/visualize') + .directive('visEditorAggGroupWrapper', reactDirective => + reactDirective(wrapInI18nContext(DefaultEditorAggGroup), [ + ['metricAggs', { watchDepth: 'reference' }], // we watch reference to identify each aggs change in useEffects + ['schemas', { watchDepth: 'collection' }], + ['state', { watchDepth: 'reference' }], + ['addSchema', { watchDepth: 'reference' }], + ['onAggParamsChange', { watchDepth: 'reference' }], + ['onAggTypeChange', { watchDepth: 'reference' }], + ['onToggleEnableAgg', { watchDepth: 'reference' }], + ['removeAgg', { watchDepth: 'reference' }], + ['reorderAggs', { watchDepth: 'reference' }], + ['setTouched', { watchDepth: 'reference' }], + ['setValidity', { watchDepth: 'reference' }], + 'groupName', + 'formIsTouched', + 'lastParentPipelineAggTitle', + ]) + ) .directive('visEditorAggGroup', function () { - return { restrict: 'E', - template: aggGroupTemplate, scope: true, - link: function ($scope, $el, attr) { + require: '?^ngModel', + template: function () { + return ``; + }, + link: function ($scope, $el, attr, ngModelCtrl) { $scope.groupName = attr.groupName; - $scope.groupNameLabel = aggGroupNameMaps()[$scope.groupName]; - $scope.$bind('group', 'state.aggs.bySchemaGroup["' + $scope.groupName + '"]'); - $scope.$bind('schemas', 'vis.type.schemas["' + $scope.groupName + '"]'); - - $scope.$watchMulti([ - 'schemas', - '[]group' - ], function () { - const stats = $scope.stats = { - min: 0, - max: 0, - count: $scope.group ? $scope.group.length : 0 - }; - - if (!$scope.schemas) return; + $scope.$bind('schemas', attr.schemas); + // The model can become touched either onBlur event or when the form is submitted. + // We also watch $touched to identify when the form is submitted. + $scope.$watch( + () => { + return ngModelCtrl.$touched; + }, + value => { + $scope.formIsTouched = value; + } + ); - $scope.schemas.forEach(function (schema) { - stats.min += schema.min; - stats.max += schema.max; - stats.deprecate = schema.deprecate; - }); - }); - - function reorderFinished() { - //the aggs have been reordered in [group] and we need - //to apply that ordering to [vis.aggs] - const indexOffset = $scope.state.aggs.indexOf($scope.group[0]); - _.forEach($scope.group, (agg, index) => { - move($scope.state.aggs, agg, indexOffset + index); - }); - } - - $scope.$on('agg-reorder', reorderFinished); - $scope.$on('agg-drag-start', () => $scope.dragging = true); - $scope.$on('agg-drag-end', () => { - $scope.dragging = false; - reorderFinished(); - }); - - $scope.addSchema = function (schema) { - const aggConfig = new AggConfig($scope.state.aggs, { - schema, - id: AggConfig.nextId($scope.state.aggs), - }); - aggConfig.brandNew = true; + $scope.setValidity = isValid => { + ngModelCtrl.$setValidity(`aggGroup${$scope.groupName}`, isValid); + }; - $scope.state.aggs.push(aggConfig); + $scope.setTouched = isTouched => { + if (isTouched) { + ngModelCtrl.$setTouched(); + } else { + ngModelCtrl.$setUntouched(); + } }; - } + }, }; - }); diff --git a/src/legacy/ui/public/vis/editors/default/agg_group_names.js b/src/legacy/ui/public/vis/editors/default/agg_group_names.js deleted file mode 100644 index e67f0459ff76..000000000000 --- a/src/legacy/ui/public/vis/editors/default/agg_group_names.js +++ /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 { i18n } from '@kbn/i18n'; - -export const aggGroupNameMaps = () => ({ - metrics: i18n.translate('common.ui.vis.editors.aggGroups.metricsText', { defaultMessage: 'metrics' }), - buckets: i18n.translate('common.ui.vis.editors.aggGroups.bucketsText', { defaultMessage: 'buckets' }) -}); diff --git a/src/legacy/ui/public/vis/editors/default/agg_groups.ts b/src/legacy/ui/public/vis/editors/default/agg_groups.ts index 9bfa99f8d4c9..f55e6ecd7915 100644 --- a/src/legacy/ui/public/vis/editors/default/agg_groups.ts +++ b/src/legacy/ui/public/vis/editors/default/agg_groups.ts @@ -17,7 +17,18 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; + export enum AggGroupNames { Buckets = 'buckets', Metrics = 'metrics', } + +export const aggGroupNamesMap = () => ({ + [AggGroupNames.Metrics]: i18n.translate('common.ui.vis.editors.aggGroups.metricsText', { + defaultMessage: 'Metrics', + }), + [AggGroupNames.Buckets]: i18n.translate('common.ui.vis.editors.aggGroups.bucketsText', { + defaultMessage: 'Buckets', + }), +}); diff --git a/src/legacy/ui/public/vis/editors/default/agg_params.js b/src/legacy/ui/public/vis/editors/default/agg_params.js deleted file mode 100644 index fe942d9eb227..000000000000 --- a/src/legacy/ui/public/vis/editors/default/agg_params.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. - */ - -import 'ngreact'; -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from '../../../modules'; -import { DefaultEditorAggParams } from './components/default_editor_agg_params'; - -uiModules - .get('app/visualize') - .directive('visEditorAggParams', reactDirective => reactDirective(wrapInI18nContext(DefaultEditorAggParams), [ - ['agg', { watchDepth: 'reference' }], - ['aggParams', { watchDepth: 'collection' }], - ['indexPattern', { watchDepth: 'reference' }], - ['metricAggs', { watchDepth: 'reference' }], // we watch reference to identify each aggs change in useEffects - ['state', { watchDepth: 'reference' }], - ['onAggTypeChange', { watchDepth: 'reference' }], - ['onAggParamsChange', { watchDepth: 'reference' }], - ['setTouched', { watchDepth: 'reference' }], - ['setValidity', { watchDepth: 'reference' }], - 'aggError', - 'aggIndex', - 'disabledParams', - 'groupName', - 'aggIsTooLow', - 'formIsTouched', - ])); diff --git a/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg.test.tsx.snap new file mode 100644 index 000000000000..5790e0d4e872 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DefaultEditorAgg component should init with the default set of props 1`] = ` + + Schema name + + + } + buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + className="visEditorSidebar__section visEditorSidebar__collapsible visEditorSidebar__collapsible--marginBottom" + data-test-subj="visEditorAggAccordion1" + extraAction={ +
    + + + +
    + } + id="visEditorAggAccordion1" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" +> + + +
    +`; diff --git a/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_group.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_group.test.tsx.snap new file mode 100644 index 000000000000..813b7978d266 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_group.test.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DefaultEditorAgg component should init with the default set of props 1`] = ` + + + +
    + Metrics +
    +
    + + + + + + + + + +
    +
    +`; diff --git a/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_params.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_params.test.tsx.snap new file mode 100644 index 000000000000..018fe0b7dbd3 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_params.test.tsx.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DefaultEditorAggParams component should init with the default set of params 1`] = ` + + + + + + + + + + +`; diff --git a/src/legacy/ui/public/vis/editors/default/components/__snapshots__/default_editor_agg_params.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/default_editor_agg_params.test.tsx.snap deleted file mode 100644 index b4d796443b55..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/__snapshots__/default_editor_agg_params.test.tsx.snap +++ /dev/null @@ -1,84 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DefaultEditorAggParams component should init with the default set of params 1`] = ` - - - - - - - - - -`; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx new file mode 100644 index 000000000000..a583fffe6d92 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx @@ -0,0 +1,279 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { mount, shallow } from 'enzyme'; +import { VisState } from '../../..'; +import { AggGroupNames } from '../agg_groups'; +import { DefaultEditorAgg, DefaultEditorAggProps } from './agg'; +import { act } from 'react-dom/test-utils'; +import { DefaultEditorAggParams } from './agg_params'; + +jest.mock('./agg_params', () => ({ + DefaultEditorAggParams: () => null, +})); + +describe('DefaultEditorAgg component', () => { + let defaultProps: DefaultEditorAggProps; + let onAggParamsChange: jest.Mock; + let setTouched: jest.Mock; + let onToggleEnableAgg: jest.Mock; + let removeAgg: jest.Mock; + let setValidity: jest.Mock; + + beforeEach(() => { + onAggParamsChange = jest.fn(); + setTouched = jest.fn(); + onToggleEnableAgg = jest.fn(); + removeAgg = jest.fn(); + setValidity = jest.fn(); + + defaultProps = { + agg: { + id: 1, + brandNew: true, + getIndexPattern: () => ({}), + schema: { title: 'Schema name' }, + title: 'Metrics', + params: {}, + }, + aggIndex: 0, + aggIsTooLow: false, + dragHandleProps: null, + formIsTouched: false, + groupName: AggGroupNames.Metrics, + isDraggable: false, + isLastBucket: false, + isRemovable: false, + metricAggs: [], + state: {} as VisState, + onAggParamsChange, + onAggTypeChange: () => {}, + setValidity, + setTouched, + onToggleEnableAgg, + removeAgg, + }; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should open accordion initially', () => { + const comp = shallow(); + + expect(comp.props()).toHaveProperty('initialIsOpen', true); + }); + + it('should not show description when agg is invalid', () => { + defaultProps.agg.brandNew = false; + const comp = mount(); + + act(() => { + comp + .find(DefaultEditorAggParams) + .props() + .setValidity(false); + }); + comp.update(); + expect(setValidity).toBeCalledWith(false); + + expect( + comp.find('.visEditorSidebar__aggGroupAccordionButtonContent span').exists() + ).toBeFalsy(); + }); + + it('should show description when agg is valid', () => { + defaultProps.agg.brandNew = false; + defaultProps.agg.type = { + makeLabel: () => 'Agg description', + }; + const comp = mount(); + + act(() => { + comp + .find(DefaultEditorAggParams) + .props() + .setValidity(true); + }); + comp.update(); + expect(setValidity).toBeCalledWith(true); + + expect(comp.find('.visEditorSidebar__aggGroupAccordionButtonContent span').text()).toBe( + 'Agg description' + ); + }); + + it('should call setTouched when accordion is collapsed', () => { + const comp = mount(); + expect(defaultProps.setTouched).toBeCalledTimes(0); + + comp.find('.euiAccordion__button').simulate('click'); + // make sure that the accordion is collapsed + expect(comp.find('.euiAccordion-isOpen').exists()).toBeFalsy(); + + expect(defaultProps.setTouched).toBeCalledWith(true); + }); + + it('should call setValidity inside onSetValidity', () => { + const comp = mount(); + + act(() => { + comp + .find(DefaultEditorAggParams) + .props() + .setValidity(false); + }); + + expect(setValidity).toBeCalledWith(false); + + expect( + comp.find('.visEditorSidebar__aggGroupAccordionButtonContent span').exists() + ).toBeFalsy(); + }); + + it('should add schema component', () => { + defaultProps.agg.schema = { + editorComponent: () =>
    , + }; + const comp = mount(); + + expect(comp.find('.schemaComponent').exists()).toBeTruthy(); + }); + + describe('agg actions', () => { + beforeEach(() => { + defaultProps.agg.enabled = true; + }); + + it('should not have actions', () => { + const comp = shallow(); + const actions = shallow(comp.prop('extraAction')); + + expect(actions.children().exists()).toBeFalsy(); + }); + + it('should have disable and remove actions', () => { + defaultProps.isRemovable = true; + const comp = mount(); + + expect( + comp.find('[data-test-subj="toggleDisableAggregationBtn disable"] button').exists() + ).toBeTruthy(); + expect(comp.find('[data-test-subj="removeDimensionBtn"] button').exists()).toBeTruthy(); + }); + + it('should have draggable action', () => { + defaultProps.isDraggable = true; + const comp = mount(); + + expect(comp.find('[data-test-subj="dragHandleBtn"]').exists()).toBeTruthy(); + }); + + it('should disable agg', () => { + defaultProps.isRemovable = true; + const comp = mount(); + comp.find('[data-test-subj="toggleDisableAggregationBtn disable"] button').simulate('click'); + + expect(defaultProps.onToggleEnableAgg).toBeCalledWith(defaultProps.agg, false); + }); + + it('should enable agg', () => { + defaultProps.agg.enabled = false; + const comp = mount(); + comp.find('[data-test-subj="toggleDisableAggregationBtn enable"] button').simulate('click'); + + expect(defaultProps.onToggleEnableAgg).toBeCalledWith(defaultProps.agg, true); + }); + + it('should call removeAgg', () => { + defaultProps.isRemovable = true; + const comp = mount(); + comp.find('[data-test-subj="removeDimensionBtn"] button').simulate('click'); + + expect(defaultProps.removeAgg).toBeCalledWith(defaultProps.agg); + }); + }); + + describe('last bucket', () => { + beforeEach(() => { + defaultProps.isLastBucket = true; + defaultProps.lastParentPipelineAggTitle = 'ParentPipelineAgg'; + }); + + it('should disable min_doc_count when agg is histogram or date_histogram', () => { + defaultProps.agg.type = { + name: 'histogram', + }; + const compHistogram = shallow(); + defaultProps.agg.type = { + name: 'date_histogram', + }; + const compDateHistogram = shallow(); + + expect(compHistogram.find(DefaultEditorAggParams).props()).toHaveProperty('disabledParams', [ + 'min_doc_count', + ]); + expect(compDateHistogram.find(DefaultEditorAggParams).props()).toHaveProperty( + 'disabledParams', + ['min_doc_count'] + ); + }); + + it('should set error when agg is not histogram or date_histogram', () => { + defaultProps.agg.type = { + name: 'aggType', + }; + const comp = shallow(); + + expect(comp.find(DefaultEditorAggParams).prop('aggError')).toBeDefined(); + }); + + it('should set min_doc_count to true when agg type was changed to histogram', () => { + defaultProps.agg.type = { + name: 'aggType', + }; + const comp = mount(); + comp.setProps({ agg: { ...defaultProps.agg, type: { name: 'histogram' } } }); + + expect(defaultProps.onAggParamsChange).toHaveBeenCalledWith( + defaultProps.agg.params, + 'min_doc_count', + true + ); + }); + + it('should set min_doc_count to 0 when agg type was changed to date_histogram', () => { + defaultProps.agg.type = { + name: 'aggType', + }; + const comp = mount(); + comp.setProps({ agg: { ...defaultProps.agg, type: { name: 'date_histogram' } } }); + + expect(defaultProps.onAggParamsChange).toHaveBeenCalledWith( + defaultProps.agg.params, + 'min_doc_count', + 0 + ); + }); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg.tsx b/src/legacy/ui/public/vis/editors/default/components/agg.tsx new file mode 100644 index 000000000000..e510c8313a10 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg.tsx @@ -0,0 +1,263 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, { useState, useEffect } from 'react'; +import { + EuiAccordion, + EuiToolTip, + EuiButtonIcon, + EuiSpacer, + EuiIconTip, + Color, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { AggConfig } from '../../..'; +import { DefaultEditorAggParams } from './agg_params'; +import { DefaultEditorAggCommonProps } from './agg_common_props'; + +export interface DefaultEditorAggProps extends DefaultEditorAggCommonProps { + agg: AggConfig; + aggIndex: number; + aggIsTooLow: boolean; + dragHandleProps: {} | null; + isDraggable: boolean; + isLastBucket: boolean; + isRemovable: boolean; +} + +function DefaultEditorAgg({ + agg, + aggIndex, + aggIsTooLow, + dragHandleProps, + formIsTouched, + groupName, + isDraggable, + isLastBucket, + isRemovable, + metricAggs, + lastParentPipelineAggTitle, + state, + onAggParamsChange, + onAggTypeChange, + onToggleEnableAgg, + removeAgg, + setTouched, + setValidity, +}: DefaultEditorAggProps) { + const [isEditorOpen, setIsEditorOpen] = useState(agg.brandNew); + const [validState, setValidState] = useState(true); + const showDescription = !isEditorOpen && validState; + const showError = !isEditorOpen && !validState; + let disabledParams; + let aggError; + // When a Parent Pipeline agg is selected and this agg is the last bucket. + const isLastBucketAgg = isLastBucket && lastParentPipelineAggTitle && agg.type; + + const SchemaComponent = agg.schema.editorComponent; + + if (isLastBucketAgg) { + if (['date_histogram', 'histogram'].includes(agg.type.name)) { + disabledParams = ['min_doc_count']; + } else { + aggError = i18n.translate('common.ui.aggTypes.metrics.wrongLastBucketTypeErrorMessage', { + defaultMessage: + 'Last bucket aggregation must be "Date Histogram" or "Histogram" when using "{type}" metric aggregation.', + values: { type: lastParentPipelineAggTitle }, + description: 'Date Histogram and Histogram should not be translated', + }); + } + } + + useEffect(() => { + if (isLastBucketAgg && ['date_histogram', 'histogram'].includes(agg.type.name)) { + onAggParamsChange( + agg.params, + 'min_doc_count', + // "histogram" agg has an editor for "min_doc_count" param, which accepts boolean + // "date_histogram" agg doesn't have an editor for "min_doc_count" param, it should be set as a numeric value + agg.type.name === 'histogram' ? true : 0 + ); + } + }, [lastParentPipelineAggTitle, isLastBucket, agg.type]); + + // A description of the aggregation, for displaying in the collapsed agg header + const aggDescription = agg.type && agg.type.makeLabel ? agg.type.makeLabel(agg) : ''; + + const onToggle = (isOpen: boolean) => { + setIsEditorOpen(isOpen); + if (!isOpen) { + setTouched(true); + } + }; + + const onSetValidity = (isValid: boolean) => { + setValidity(isValid); + setValidState(isValid); + }; + + const renderAggButtons = () => { + const actionIcons = []; + + if (showError) { + actionIcons.push({ + id: 'hasErrors', + color: 'danger', + type: 'alert', + tooltip: i18n.translate('common.ui.vis.editors.agg.errorsAriaLabel', { + defaultMessage: 'Aggregation has errors', + }), + dataTestSubj: 'hasErrorsAggregationIcon', + }); + } + + if (agg.enabled && isRemovable) { + actionIcons.push({ + id: 'disableAggregation', + color: 'text', + type: 'eye', + onClick: () => onToggleEnableAgg(agg, false), + tooltip: i18n.translate('common.ui.vis.editors.agg.disableAggButtonTooltip', { + defaultMessage: 'Disable aggregation', + }), + dataTestSubj: 'toggleDisableAggregationBtn disable', + }); + } + if (!agg.enabled) { + actionIcons.push({ + id: 'enableAggregation', + color: 'text', + type: 'eyeClosed', + onClick: () => onToggleEnableAgg(agg, true), + tooltip: i18n.translate('common.ui.vis.editors.agg.enableAggButtonTooltip', { + defaultMessage: 'Enable aggregation', + }), + dataTestSubj: 'toggleDisableAggregationBtn enable', + }); + } + if (isDraggable) { + actionIcons.push({ + id: 'dragHandle', + type: 'grab', + tooltip: i18n.translate('common.ui.vis.editors.agg.modifyPriorityButtonTooltip', { + defaultMessage: 'Modify priority by dragging', + }), + dataTestSubj: 'dragHandleBtn', + }); + } + if (isRemovable) { + actionIcons.push({ + id: 'removeDimension', + color: 'danger', + type: 'cross', + onClick: () => removeAgg(agg), + tooltip: i18n.translate('common.ui.vis.editors.agg.removeDimensionButtonTooltip', { + defaultMessage: 'Remove dimension', + }), + dataTestSubj: 'removeDimensionBtn', + }); + } + return ( +
    + {actionIcons.map(icon => { + if (icon.id === 'dragHandle') { + return ( + + ); + } + + return ( + + + + ); + })} +
    + ); + }; + + const buttonContent = ( + <> + {agg.schema.title} {showDescription && {aggDescription}} + + ); + + return ( + + <> + + {SchemaComponent && ( + + )} + + + + ); +} + +export { DefaultEditorAgg }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_add.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_add.tsx new file mode 100644 index 000000000000..21ee5b507e3b --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_add.tsx @@ -0,0 +1,126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiPopoverTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { AggConfig } from '../../..'; +import { Schema } from '../schemas'; +import { AggGroupNames } from '../agg_groups'; + +interface DefaultEditorAggAddProps { + group?: AggConfig[]; + groupName: string; + schemas: Schema[]; + stats: { + max: number; + count: number; + }; + addSchema(schema: Schema): void; +} + +function DefaultEditorAggAdd({ + group = [], + groupName, + schemas, + addSchema, + stats, +}: DefaultEditorAggAddProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onSelectSchema = (schema: Schema) => { + setIsPopoverOpen(false); + addSchema(schema); + }; + + const addButton = ( + setIsPopoverOpen(!isPopoverOpen)} + > + + + ); + + const groupNameLabel = + groupName === AggGroupNames.Buckets + ? i18n.translate('common.ui.vis.editors.aggAdd.bucketLabel', { defaultMessage: 'bucket' }) + : i18n.translate('common.ui.vis.editors.aggAdd.metricLabel', { defaultMessage: 'metric' }); + + const isSchemaDisabled = (schema: Schema): boolean => { + const count = group.filter(agg => agg.schema.name === schema.name).length; + return count >= schema.max; + }; + + return ( + + + setIsPopoverOpen(false)} + > + + {(groupName !== AggGroupNames.Buckets || !stats.count) && ( + + )} + {groupName === AggGroupNames.Buckets && stats.count > 0 && ( + + )} + + ( + onSelectSchema(schema)} + > + {schema.title} + + ))} + /> + + + + ); +} + +export { DefaultEditorAggAdd }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts b/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts new file mode 100644 index 000000000000..1a057f0c2170 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { AggType } from 'ui/agg_types'; +import { AggConfig, VisState, VisParams } from '../../..'; +import { AggParams } from '../agg_params'; +import { AggGroupNames } from '../agg_groups'; + +export type OnAggParamsChange = ( + params: AggParams | VisParams, + paramName: string, + value: unknown +) => void; + +export interface DefaultEditorAggCommonProps { + formIsTouched: boolean; + groupName: AggGroupNames; + lastParentPipelineAggTitle?: string; + metricAggs: AggConfig[]; + state: VisState; + onAggParamsChange: OnAggParamsChange; + onAggTypeChange: (agg: AggConfig, aggType: AggType) => void; + onToggleEnableAgg: (agg: AggConfig, isEnable: boolean) => void; + removeAgg: (agg: AggConfig) => void; + setTouched: (isTouched: boolean) => void; + setValidity: (isValid: boolean) => void; +} diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group.test.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_group.test.tsx new file mode 100644 index 000000000000..508365ba626a --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group.test.tsx @@ -0,0 +1,220 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { mount, shallow } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { VisState, AggConfig } from '../../../'; +import { Schema } from '../schemas'; +import { AggGroupNames } from '../agg_groups'; +import { AggConfigs } from '../../../agg_configs'; +import { DefaultEditorAggGroup, DefaultEditorAggGroupProps } from './agg_group'; +import { DefaultEditorAgg } from './agg'; +import { DefaultEditorAggAdd } from './agg_add'; + +jest.mock('@elastic/eui', () => ({ + EuiTitle: 'eui-title', + EuiDragDropContext: 'eui-drag-drop-context', + EuiDroppable: 'eui-droppable', + EuiDraggable: (props: any) => props.children({ dragHandleProps: {} }), + EuiSpacer: 'eui-spacer', + EuiPanel: 'eui-panel', +})); + +jest.mock('./agg', () => ({ + DefaultEditorAgg: () =>
    , +})); + +jest.mock('./agg_add', () => ({ + DefaultEditorAggAdd: () =>
    , +})); + +describe('DefaultEditorAgg component', () => { + let defaultProps: DefaultEditorAggGroupProps; + let aggs: AggConfigs; + let setTouched: jest.Mock; + let setValidity: jest.Mock; + let reorderAggs: jest.Mock; + + beforeEach(() => { + setTouched = jest.fn(); + setValidity = jest.fn(); + reorderAggs = jest.fn(); + + aggs = [ + { + id: 1, + title: 'Metrics', + params: { + field: { + type: 'number', + }, + }, + group: 'metrics', + schema: {}, + }, + { + id: 3, + title: 'Agg', + params: { + field: { + type: 'string', + }, + }, + group: 'metrics', + schema: {}, + }, + { + id: 2, + title: 'Buckets', + params: { + field: { + type: 'number', + }, + }, + group: 'buckets', + schema: {}, + }, + ] as AggConfigs; + + Object.defineProperty(aggs, 'bySchemaGroup', { + get: () => + aggs.reduce((acc: { [key: string]: AggConfig }, option: AggConfig) => { + if (acc[option.group]) { + acc[option.group].push(option); + } else { + acc[option.group] = [option]; + } + + return acc; + }, {}), + }); + + defaultProps = { + formIsTouched: false, + metricAggs: [], + groupName: AggGroupNames.Metrics, + state: { + aggs, + } as VisState, + schemas: [ + { + max: 1, + } as Schema, + { + max: 1, + } as Schema, + ], + setTouched, + setValidity, + reorderAggs, + addSchema: () => {}, + removeAgg: () => {}, + onAggParamsChange: () => {}, + onAggTypeChange: () => {}, + onToggleEnableAgg: () => {}, + }; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should call setTouched with false', () => { + mount(); + + expect(setTouched).toBeCalledWith(false); + }); + + it('should mark group as touched when all invalid aggs are touched', () => { + defaultProps.groupName = AggGroupNames.Buckets; + const comp = mount(); + act(() => { + const aggProps = comp.find(DefaultEditorAgg).props(); + aggProps.setValidity(false); + aggProps.setTouched(true); + }); + + expect(setTouched).toBeCalledWith(true); + }); + + it('should mark group as touched when the form applied', () => { + const comp = mount(); + act(() => { + comp + .find(DefaultEditorAgg) + .first() + .props() + .setValidity(false); + }); + expect(setTouched).toBeCalledWith(false); + comp.setProps({ formIsTouched: true }); + + expect(setTouched).toBeCalledWith(true); + }); + + it('should mark group as invalid when at least one agg is invalid', () => { + const comp = mount(); + act(() => { + comp + .find(DefaultEditorAgg) + .first() + .props() + .setValidity(false); + }); + + expect(setValidity).toBeCalledWith(false); + }); + + it('should last bucket has truthy isLastBucket prop', () => { + defaultProps.groupName = AggGroupNames.Buckets; + const comp = mount(); + const lastAgg = comp.find(DefaultEditorAgg).last(); + + expect(lastAgg.props()).toHaveProperty('isLastBucket', true); + }); + + it('should call reorderAggs when dragging ended', () => { + const comp = shallow(); + act(() => { + // simulate dragging ending + comp.props().onDragEnd({ source: { index: 0 }, destination: { index: 1 } }); + }); + + expect(reorderAggs).toHaveBeenCalledWith([ + defaultProps.state.aggs[1], + defaultProps.state.aggs[0], + ]); + }); + + it('should show add button when schemas count is less than max', () => { + defaultProps.groupName = AggGroupNames.Buckets; + const comp = shallow(); + + expect(comp.find(DefaultEditorAggAdd).exists()).toBeTruthy(); + }); + + it('should not show add button when schemas count is not less than max', () => { + const comp = shallow(); + + expect(comp.find(DefaultEditorAggAdd).exists()).toBeFalsy(); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_group.tsx new file mode 100644 index 000000000000..6c51643833b9 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group.tsx @@ -0,0 +1,189 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, useReducer } from 'react'; +import { + EuiTitle, + EuiDragDropContext, + EuiDroppable, + EuiDraggable, + EuiSpacer, + EuiPanel, +} from '@elastic/eui'; + +import { AggConfig } from '../../../agg_config'; +import { aggGroupNamesMap, AggGroupNames } from '../agg_groups'; +import { DefaultEditorAgg } from './agg'; +import { DefaultEditorAggAdd } from './agg_add'; +import { DefaultEditorAggCommonProps } from './agg_common_props'; +import { isInvalidAggsTouched, isAggRemovable, calcAggIsTooLow } from './agg_group_helper'; +import { aggGroupReducer, initAggsState, AGGS_ACTION_KEYS } from './agg_group_state'; +import { Schema } from '../schemas'; + +export interface DefaultEditorAggGroupProps extends DefaultEditorAggCommonProps { + schemas: Schema[]; + addSchema: (schems: Schema) => void; + reorderAggs: (group: AggConfig[]) => void; +} + +function DefaultEditorAggGroup({ + formIsTouched, + groupName, + lastParentPipelineAggTitle, + metricAggs, + state, + schemas = [], + addSchema, + onAggParamsChange, + onAggTypeChange, + onToggleEnableAgg, + removeAgg, + reorderAggs, + setTouched, + setValidity, +}: DefaultEditorAggGroupProps) { + const groupNameLabel = aggGroupNamesMap()[groupName]; + // e.g. buckets can have no aggs + const group: AggConfig[] = state.aggs.bySchemaGroup[groupName] || []; + + const stats = { + max: 0, + count: group.length, + }; + + schemas.forEach((schema: Schema) => { + stats.max += schema.max; + }); + + const [aggsState, setAggsState] = useReducer(aggGroupReducer, group, initAggsState); + + const isGroupValid = Object.values(aggsState).every(item => item.valid); + const isAllAggsTouched = isInvalidAggsTouched(aggsState); + + useEffect(() => { + // when isAllAggsTouched is true, it means that all invalid aggs are touched and we will set ngModel's touched to true + // which indicates that Apply button can be changed to Error button (when all invalid ngModels are touched) + setTouched(isAllAggsTouched); + }, [isAllAggsTouched]); + + useEffect(() => { + // when not all invalid aggs are touched and formIsTouched becomes true, it means that Apply button was clicked. + // and in such case we set touched state to true for all aggs + if (formIsTouched && !isAllAggsTouched) { + Object.keys(aggsState).map(([aggId]) => { + setAggsState({ + type: AGGS_ACTION_KEYS.TOUCHED, + payload: true, + aggId: Number(aggId), + }); + }); + } + }, [formIsTouched]); + + useEffect(() => { + setValidity(isGroupValid); + }, [isGroupValid]); + + interface DragDropResultProps { + source: { index: number }; + destination?: { index: number } | null; + } + const onDragEnd = ({ source, destination }: DragDropResultProps) => { + if (source && destination) { + const orderedGroup = Array.from(group); + const [removed] = orderedGroup.splice(source.index, 1); + orderedGroup.splice(destination.index, 0, removed); + + reorderAggs(orderedGroup); + } + }; + + const setTouchedHandler = (aggId: number, touched: boolean) => { + setAggsState({ + type: AGGS_ACTION_KEYS.TOUCHED, + payload: touched, + aggId, + }); + }; + + const setValidityHandler = (aggId: number, valid: boolean) => { + setAggsState({ + type: AGGS_ACTION_KEYS.VALID, + payload: valid, + aggId, + }); + }; + + return ( + + + +
    {groupNameLabel}
    +
    + + + <> + {group.map((agg: AggConfig, index: number) => ( + + {provided => ( + 1} + isLastBucket={groupName === AggGroupNames.Buckets && index === group.length - 1} + isRemovable={isAggRemovable(agg, group)} + lastParentPipelineAggTitle={lastParentPipelineAggTitle} + metricAggs={metricAggs} + state={state} + onAggParamsChange={onAggParamsChange} + onAggTypeChange={onAggTypeChange} + onToggleEnableAgg={onToggleEnableAgg} + removeAgg={removeAgg} + setTouched={isTouched => setTouchedHandler(agg.id, isTouched)} + setValidity={isValid => setValidityHandler(agg.id, isValid)} + /> + )} + + ))} + + + {stats.max > stats.count && ( + + )} +
    +
    + ); +} + +export { DefaultEditorAggGroup }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.test.ts b/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.test.ts new file mode 100644 index 000000000000..e2317ccb1f32 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.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 { AggConfig } from '../../../agg_config'; +import { isAggRemovable, calcAggIsTooLow, isInvalidAggsTouched } from './agg_group_helper'; +import { AggsState } from './agg_group_state'; + +describe('DefaultEditorGroup helpers', () => { + let group: AggConfig; + + beforeEach(() => { + group = [ + { + id: 1, + title: 'Test1', + params: { + field: { + type: 'number', + }, + }, + group: 'metrics', + schema: { name: 'metric', min: 1, mustBeFirst: true }, + }, + { + id: 2, + title: 'Test2', + params: { + field: { + type: 'string', + }, + }, + group: 'metrics', + schema: { name: 'metric', min: 2 }, + }, + ]; + }); + describe('isAggRemovable', () => { + it('should return true when the number of aggs with the same schema is above the min', () => { + const isRemovable = isAggRemovable(group[0], group); + + expect(isRemovable).toBeTruthy(); + }); + + it('should return false when the number of aggs with the same schema is not above the min', () => { + const isRemovable = isAggRemovable(group[1], group); + + expect(isRemovable).toBeFalsy(); + }); + }); + + describe('calcAggIsTooLow', () => { + it('should return false when agg.schema.mustBeFirst has falsy value', () => { + const isRemovable = calcAggIsTooLow(group[1], 0, group); + + expect(isRemovable).toBeFalsy(); + }); + + it('should return false when there is no different schema', () => { + group[1].schema = group[0].schema; + const isRemovable = calcAggIsTooLow(group[0], 0, group); + + expect(isRemovable).toBeFalsy(); + }); + + it('should return false when different schema is not less than agg index', () => { + const isRemovable = calcAggIsTooLow(group[0], 0, group); + + expect(isRemovable).toBeFalsy(); + }); + + it('should return true when agg index is greater than different schema index', () => { + const isRemovable = calcAggIsTooLow(group[0], 2, group); + + expect(isRemovable).toBeTruthy(); + }); + }); + + describe('isInvalidAggsTouched', () => { + let aggsState: AggsState; + + beforeEach(() => { + aggsState = { + 1: { + valid: true, + touched: false, + }, + 2: { + valid: true, + touched: false, + }, + 3: { + valid: true, + touched: false, + }, + }; + }); + + it('should return false when there are no invalid aggs', () => { + const isAllInvalidAggsTouched = isInvalidAggsTouched(aggsState); + + expect(isAllInvalidAggsTouched).toBeFalsy(); + }); + + it('should return false when not all invalid aggs are touched', () => { + aggsState[1].valid = false; + const isAllInvalidAggsTouched = isInvalidAggsTouched(aggsState); + + expect(isAllInvalidAggsTouched).toBeFalsy(); + }); + + it('should return true when all invalid aggs are touched', () => { + aggsState[1].valid = false; + aggsState[1].touched = true; + const isAllInvalidAggsTouched = isInvalidAggsTouched(aggsState); + + expect(isAllInvalidAggsTouched).toBeTruthy(); + }); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.tsx new file mode 100644 index 000000000000..99b913d8f94c --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { findIndex, reduce, isEmpty } from 'lodash'; +import { AggConfig } from '../../../agg_config'; +import { AggsState } from './agg_group_state'; + +const isAggRemovable = (agg: AggConfig, group: AggConfig[]) => { + const metricCount = reduce( + group, + (count, aggregation: AggConfig) => { + return aggregation.schema.name === agg.schema.name ? ++count : count; + }, + 0 + ); + // make sure the the number of these aggs is above the min + return metricCount > agg.schema.min; +}; + +const calcAggIsTooLow = (agg: AggConfig, aggIndex: number, group: AggConfig[]) => { + if (!agg.schema.mustBeFirst) { + return false; + } + + const firstDifferentSchema = findIndex(group, (aggr: AggConfig) => { + return aggr.schema !== agg.schema; + }); + + if (firstDifferentSchema === -1) { + return false; + } + + return aggIndex > firstDifferentSchema; +}; + +function isInvalidAggsTouched(aggsState: AggsState) { + const invalidAggs = Object.values(aggsState).filter(agg => !agg.valid); + + if (isEmpty(invalidAggs)) { + return false; + } + + return invalidAggs.every(agg => agg.touched); +} + +export { isAggRemovable, calcAggIsTooLow, isInvalidAggsTouched }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx new file mode 100644 index 000000000000..cba7f09a2be0 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { AggConfig } from '../../../agg_config'; + +export enum AGGS_ACTION_KEYS { + TOUCHED = 'aggsTouched', + VALID = 'aggsValid', +} + +interface AggsItem { + touched: boolean; + valid: boolean; +} + +export interface AggsState { + [aggId: number]: AggsItem; +} + +interface AggsAction { + type: AGGS_ACTION_KEYS; + payload: boolean; + aggId: number; + newState?: AggsState; +} + +function aggGroupReducer(state: AggsState, action: AggsAction): AggsState { + const aggState = state[action.aggId] || { touched: false, valid: true }; + switch (action.type) { + case AGGS_ACTION_KEYS.TOUCHED: + return { ...state, [action.aggId]: { ...aggState, touched: action.payload } }; + case AGGS_ACTION_KEYS.VALID: + return { ...state, [action.aggId]: { ...aggState, valid: action.payload } }; + default: + throw new Error(); + } +} + +function initAggsState(group: AggConfig[]): AggsState { + return group.reduce((state, agg) => { + state[agg.id] = { touched: false, valid: true }; + return state; + }, {}); +} + +export { aggGroupReducer, initAggsState }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_param.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_param.tsx new file mode 100644 index 000000000000..72c802d7d237 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_param.tsx @@ -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 React, { useEffect } from 'react'; + +import { AggParamEditorProps, AggParamCommonProps } from './agg_param_props'; +import { OnAggParamsChange } from './agg_common_props'; + +interface DefaultEditorAggParamProps extends AggParamCommonProps { + paramEditor: React.ComponentType>; + onChange: OnAggParamsChange; +} + +function DefaultEditorAggParam(props: DefaultEditorAggParamProps) { + const { agg, aggParam, paramEditor: ParamEditor, onChange, setValidity, ...rest } = props; + + useEffect(() => { + if (aggParam.shouldShow && !aggParam.shouldShow(agg)) { + setValidity(true); + } + }, [agg, agg.params.field]); + + if (aggParam.shouldShow && !aggParam.shouldShow(agg)) { + return null; + } + + return ( + onChange(agg.params, aggParam.name, value)} + {...rest} + /> + ); +} + +export { DefaultEditorAggParam }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_param_props.ts b/src/legacy/ui/public/vis/editors/default/components/agg_param_props.ts new file mode 100644 index 000000000000..5da6f21ac6af --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_param_props.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 { AggParam, FieldParamType } from 'ui/agg_types'; +import { AggConfig } from '../../../agg_config'; +import { EditorConfig } from '../../config/types'; +import { VisState } from '../../..'; +import { SubAggParamsProp } from './agg_params'; + +// NOTE: we cannot export the interface with export { InterfaceName } +// as there is currently a bug on babel typescript transform plugin for it +// https://github.com/babel/babel/issues/7641 +// +export interface AggParamCommonProps { + agg: AggConfig; + aggParam: AggParam; + disabled?: boolean; + editorConfig: EditorConfig; + indexedFields?: FieldParamType[]; + showValidation: boolean; + state: VisState; + value?: T; + metricAggs: AggConfig[]; + subAggParams: SubAggParamsProp; + setValidity(isValid: boolean): void; + setTouched(): void; +} + +export interface AggParamEditorProps extends AggParamCommonProps { + setValue(value?: T): void; +} diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params.test.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_params.test.tsx new file mode 100644 index 000000000000..d60143ee8c65 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params.test.tsx @@ -0,0 +1,180 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { mount, shallow } from 'enzyme'; +import { IndexPattern } from 'ui/index_patterns'; +import { VisState } from '../../..'; +import { DefaultEditorAggParams, DefaultEditorAggParamsProps } from './agg_params'; + +const mockEditorConfig = { + useNormalizedEsInterval: { hidden: false, fixedValue: false }, + interval: { + hidden: false, + help: 'Must be a multiple of rollup configuration interval: 1m', + default: '1m', + timeBase: '1m', + }, +}; + +jest.mock('ui/agg_types', () => ({ + aggTypes: { + byType: { + metrics: [], + buckets: [], + }, + }, +})); +jest.mock('../../config/editor_config_providers', () => ({ + editorConfigProviders: { + getConfigForAgg: jest.fn(() => mockEditorConfig), + }, +})); +jest.mock('./agg_params_helper', () => ({ + getAggParamsToRender: jest.fn(() => ({ + basic: [ + { + aggParam: { + displayName: 'Custom label', + name: 'customLabel', + type: 'string', + }, + }, + ], + advanced: [ + { + aggParam: { + advanced: true, + name: 'json', + type: 'json', + }, + }, + ], + })), + getAggTypeOptions: jest.fn(() => []), + getError: jest.fn((agg, aggIsTooLow) => (aggIsTooLow ? ['error'] : [])), + isInvalidParamsTouched: jest.fn(() => false), +})); +jest.mock('./agg_select', () => ({ + DefaultEditorAggSelect: () => null, +})); +jest.mock('./agg_param', () => ({ + DefaultEditorAggParam: () => null, +})); + +describe('DefaultEditorAggParams component', () => { + let onAggParamsChange: jest.Mock; + let onAggTypeChange: jest.Mock; + let setTouched: jest.Mock; + let setValidity: jest.Mock; + let intervalDeserialize: jest.Mock; + let defaultProps: DefaultEditorAggParamsProps; + + beforeEach(() => { + onAggParamsChange = jest.fn(); + onAggTypeChange = jest.fn(); + setTouched = jest.fn(); + setValidity = jest.fn(); + intervalDeserialize = jest.fn(() => 'deserialized'); + + defaultProps = { + agg: { + type: { + params: [{ name: 'interval', deserialize: intervalDeserialize }], + }, + params: {}, + }, + groupName: 'metrics', + formIsTouched: false, + indexPattern: {} as IndexPattern, + metricAggs: [], + state: {} as VisState, + onAggParamsChange, + onAggTypeChange, + setTouched, + setValidity, + }; + }); + + it('should init with the default set of params', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should reset the validity to true when destroyed', () => { + const comp = mount(); + + expect(setValidity).lastCalledWith(false); + + comp.unmount(); + + expect(setValidity).lastCalledWith(true); + }); + + it('should set fixed and default values when editorConfig is defined (works in rollup index)', () => { + mount(); + + expect(onAggParamsChange).toHaveBeenNthCalledWith( + 1, + defaultProps.agg.params, + 'useNormalizedEsInterval', + false + ); + expect(intervalDeserialize).toHaveBeenCalledWith('1m'); + expect(onAggParamsChange).toHaveBeenNthCalledWith( + 2, + defaultProps.agg.params, + 'interval', + 'deserialized' + ); + }); + + it('should call setTouched with false when agg type is changed', () => { + const comp = mount(); + + comp.setProps({ agg: { type: { params: [] } } }); + + expect(setTouched).lastCalledWith(false); + }); + + it('should set the validity when it changed', () => { + const comp = mount(); + + comp.setProps({ aggIsTooLow: true }); + + expect(setValidity).lastCalledWith(false); + + comp.setProps({ aggIsTooLow: false }); + + expect(setValidity).lastCalledWith(true); + }); + + it('should call setTouched when all invalid controls were touched or they are untouched', () => { + const comp = mount(); + + comp.setProps({ aggIsTooLow: true }); + + expect(setTouched).lastCalledWith(true); + + comp.setProps({ aggIsTooLow: false }); + + expect(setTouched).lastCalledWith(false); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_params.tsx new file mode 100644 index 000000000000..29a2421121fd --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params.tsx @@ -0,0 +1,256 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, { useReducer, useEffect } from 'react'; +import { EuiForm, EuiAccordion, EuiSpacer, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { aggTypes, AggType, AggParam } from 'ui/agg_types'; +import { IndexPattern } from 'ui/index_patterns'; +import { AggConfig, VisState } from '../../..'; +import { DefaultEditorAggSelect } from './agg_select'; +import { DefaultEditorAggParam } from './agg_param'; +import { + getAggParamsToRender, + getError, + getAggTypeOptions, + ParamInstance, + isInvalidParamsTouched, +} from './agg_params_helper'; +import { + aggTypeReducer, + AGG_TYPE_ACTION_KEYS, + aggParamsReducer, + AGG_PARAMS_ACTION_KEYS, + initAggParamsState, + AggParamsItem, +} from './agg_params_state'; +import { editorConfigProviders } from '../../config/editor_config_providers'; +import { FixedParam, TimeIntervalParam, EditorParamConfig } from '../../config/types'; +// TODO: Below import is temporary, use `react-use` lib instead. +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { useUnmount } from '../../../../../../../plugins/kibana_react/public/util/use_unmount'; +import { AggGroupNames } from '../agg_groups'; +import { OnAggParamsChange } from './agg_common_props'; + +const FIXED_VALUE_PROP = 'fixedValue'; +const DEFAULT_PROP = 'default'; +type EditorParamConfigType = EditorParamConfig & { + [key: string]: unknown; +}; +export interface SubAggParamsProp { + formIsTouched: boolean; + onAggParamsChange: OnAggParamsChange; + onAggTypeChange: (agg: AggConfig, aggType: AggType) => void; +} +export interface DefaultEditorAggParamsProps extends SubAggParamsProp { + agg: AggConfig; + aggError?: string; + aggIndex?: number; + aggIsTooLow?: boolean; + className?: string; + disabledParams?: string[]; + groupName: string; + indexPattern: IndexPattern; + metricAggs: AggConfig[]; + state: VisState; + setTouched: (isTouched: boolean) => void; + setValidity: (isValid: boolean) => void; +} + +function DefaultEditorAggParams({ + agg, + aggError, + aggIndex = 0, + aggIsTooLow = false, + className, + disabledParams, + groupName, + formIsTouched, + indexPattern, + metricAggs, + state = {} as VisState, + onAggParamsChange, + onAggTypeChange, + setTouched, + setValidity, +}: DefaultEditorAggParamsProps) { + const groupedAggTypeOptions = getAggTypeOptions(agg, indexPattern, groupName); + const errors = getError(agg, aggIsTooLow); + + const editorConfig = editorConfigProviders.getConfigForAgg( + aggTypes.byType[groupName], + indexPattern, + agg + ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); + const allParams = [...params.basic, ...params.advanced]; + const [paramsState, onChangeParamsState] = useReducer( + aggParamsReducer, + allParams, + initAggParamsState + ); + const [aggType, onChangeAggType] = useReducer(aggTypeReducer, { touched: false, valid: true }); + + const isFormValid = + !errors.length && + aggType.valid && + Object.entries(paramsState).every(([, paramState]) => paramState.valid); + + const isAllInvalidParamsTouched = + !!errors.length || isInvalidParamsTouched(agg.type, aggType, paramsState); + + // reset validity before component destroyed + useUnmount(() => setValidity(true)); + + useEffect(() => { + Object.entries(editorConfig).forEach(([param, paramConfig]) => { + const paramOptions = agg.type.params.find( + (paramOption: AggParam) => paramOption.name === param + ); + + const hasFixedValue = paramConfig.hasOwnProperty(FIXED_VALUE_PROP); + const hasDefault = paramConfig.hasOwnProperty(DEFAULT_PROP); + // If the parameter has a fixed value in the config, set this value. + // Also for all supported configs we should freeze the editor for this param. + if (hasFixedValue || hasDefault) { + let newValue; + let property = FIXED_VALUE_PROP; + let typedParamConfig: EditorParamConfigType = paramConfig as FixedParam; + + if (hasDefault) { + property = DEFAULT_PROP; + typedParamConfig = paramConfig as TimeIntervalParam; + } + + if (paramOptions && paramOptions.deserialize) { + newValue = paramOptions.deserialize(typedParamConfig[property]); + } else { + newValue = typedParamConfig[property]; + } + onAggParamsChange(agg.params, param, newValue); + } + }); + }, [agg.type]); + + useEffect(() => { + setTouched(false); + }, [agg.type]); + + useEffect(() => { + setValidity(isFormValid); + }, [isFormValid, agg.type]); + + useEffect(() => { + // when all invalid controls were touched or they are untouched + setTouched(isAllInvalidParamsTouched); + }, [isAllInvalidParamsTouched]); + + const renderParam = (paramInstance: ParamInstance, model: AggParamsItem) => { + return ( + { + onChangeParamsState({ + type: AGG_PARAMS_ACTION_KEYS.VALID, + paramName: paramInstance.aggParam.name, + payload: valid, + }); + }} + // setTouched can be called from sub-agg which passes a parameter + setTouched={(isTouched: boolean = true) => { + onChangeParamsState({ + type: AGG_PARAMS_ACTION_KEYS.TOUCHED, + paramName: paramInstance.aggParam.name, + payload: isTouched, + }); + }} + subAggParams={{ + onAggParamsChange, + onAggTypeChange, + formIsTouched, + }} + {...paramInstance} + /> + ); + }; + + return ( + + = 1 && groupName === AggGroupNames.Buckets} + showValidation={formIsTouched || aggType.touched} + setValue={value => { + onAggTypeChange(agg, value); + // reset touched and valid of params + onChangeParamsState({ type: AGG_PARAMS_ACTION_KEYS.RESET }); + }} + setTouched={() => onChangeAggType({ type: AGG_TYPE_ACTION_KEYS.TOUCHED, payload: true })} + setValidity={valid => onChangeAggType({ type: AGG_TYPE_ACTION_KEYS.VALID, payload: valid })} + /> + + {params.basic.map((param: ParamInstance) => { + const model = paramsState[param.aggParam.name] || { + touched: false, + valid: true, + }; + + return renderParam(param, model); + })} + + {params.advanced.length ? ( + + + + {params.advanced.map((param: ParamInstance) => { + const model = paramsState[param.aggParam.name] || { + touched: false, + valid: true, + }; + return renderParam(param, model); + })} + + + ) : null} + + ); +} + +export { DefaultEditorAggParams }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts new file mode 100644 index 000000000000..e1e6635d4768 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts @@ -0,0 +1,224 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { AggConfig, VisState } from '../../..'; +import { FieldParamType, AggType } from 'ui/agg_types'; +import { IndexPattern } from 'ui/index_patterns'; +import { + getAggParamsToRender, + getError, + getAggTypeOptions, + isInvalidParamsTouched, +} from './agg_params_helper'; +import { EditorConfig } from '../../config/types'; + +jest.mock('ui/agg_types', () => ({ + aggTypes: { + byType: { + metrics: [], + buckets: [], + }, + }, +})); +jest.mock('../utils', () => ({ + groupAggregationsBy: jest.fn(() => ['indexedFields']), +})); + +describe('DefaultEditorAggParams helpers', () => { + describe('getAggParamsToRender', () => { + let agg: AggConfig; + let editorConfig: EditorConfig; + const state = {} as VisState; + const metricAggs: AggConfig[] = []; + const emptyParams = { + basic: [], + advanced: [], + }; + + it('should not create any param if they do not have editorComponents', () => { + agg = { + type: { + params: [{ name: 'interval' }], + }, + schema: {}, + }; + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); + + expect(params).toEqual(emptyParams); + }); + + it('should not create any param if there is no agg type', () => { + agg = {}; + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); + + expect(params).toEqual(emptyParams); + }); + + it('should not create a param if it is hidden', () => { + agg = { + type: { + params: [{ name: 'interval' }], + }, + }; + editorConfig = { + interval: { + hidden: true, + }, + }; + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); + + expect(params).toEqual(emptyParams); + }); + + it('should skip customLabel param if it is hidden', () => { + agg = { + type: { + params: [{ name: 'customLabel' }], + }, + schema: { + hideCustomLabel: true, + }, + }; + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); + + expect(params).toEqual(emptyParams); + }); + + it('should create a basic params field and orderBy', () => { + const filterFieldTypes = ['number', 'boolean', 'date']; + agg = { + type: { + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes, + getAvailableFields: jest.fn((fields: FieldParamType[]) => + fields.filter(({ type }) => filterFieldTypes.includes(type)) + ), + editorComponent: jest.fn(), + }, + { + name: 'orderBy', + editorComponent: jest.fn(), + }, + ], + }, + schema: {}, + getIndexPattern: jest.fn(() => ({ + fields: [{ name: '@timestamp', type: 'date' }, { name: 'geo_desc', type: 'string' }], + })), + params: { + orderBy: 'orderBy', + field: 'field', + }, + }; + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); + + expect(params).toEqual({ + basic: [ + { + agg, + aggParam: agg.type.params[0], + editorConfig, + indexedFields: ['indexedFields'], + paramEditor: agg.type.params[0].editorComponent, + metricAggs, + state, + value: agg.params.field, + }, + { + agg, + aggParam: agg.type.params[1], + editorConfig, + indexedFields: [], + paramEditor: agg.type.params[1].editorComponent, + metricAggs, + state, + value: agg.params.orderBy, + }, + ], + advanced: [], + }); + expect(agg.getIndexPattern).toBeCalledTimes(1); + }); + }); + + describe('getError', () => { + it('should not have any errors', () => { + const errors = getError({ schema: { title: 'Split series' } }, false); + + expect(errors).toEqual([]); + }); + + it('should push an error if an agg is too low', () => { + const errors = getError({ schema: { title: 'Split series' } }, true); + + expect(errors).toEqual(['"Split series" aggs must run before all other buckets!']); + }); + }); + + describe('getAggTypeOptions', () => { + it('should return agg type options grouped by subtype', () => { + const indexPattern = {} as IndexPattern; + const aggs = getAggTypeOptions({}, indexPattern, 'metrics'); + + expect(aggs).toEqual(['indexedFields']); + }); + }); + + describe('isInvalidParamsTouched', () => { + let aggType: AggType; + const aggTypeState = { + touched: false, + valid: true, + }; + const aggParams = { + orderBy: { + touched: true, + valid: true, + }, + orderAgg: { + touched: true, + valid: true, + }, + }; + + it('should return aggTypeState touched if there is no aggType', () => { + const isTouched = isInvalidParamsTouched(aggType, aggTypeState, aggParams); + + expect(isTouched).toBe(aggTypeState.touched); + }); + + it('should return false if there is no invalid params', () => { + aggType = 'type'; + const isTouched = isInvalidParamsTouched(aggType, aggTypeState, aggParams); + + expect(isTouched).toBeFalsy(); + }); + + it('should return true if there is an invalid param, but not every still touched', () => { + aggType = 'type'; + aggParams.orderAgg.valid = false; + const isTouched = isInvalidParamsTouched(aggType, aggTypeState, aggParams); + + expect(isTouched).toBeTruthy(); + }); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.ts b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.ts new file mode 100644 index 000000000000..c1a69d51fb07 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.ts @@ -0,0 +1,147 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { aggTypeFilters } from 'ui/agg_types/filter'; +import { IndexPattern } from 'ui/index_patterns'; +import { aggTypes, AggParam, FieldParamType, AggType } from 'ui/agg_types'; +import { aggTypeFieldFilters } from 'ui/agg_types/param_types/filter'; +import { AggConfig, VisState } from '../../..'; +import { groupAggregationsBy } from '../utils'; +import { EditorConfig } from '../../config/types'; +import { AggTypeState, AggParamsState } from './agg_params_state'; +import { AggParamEditorProps } from './agg_param_props'; + +interface ParamInstanceBase { + agg: AggConfig; + editorConfig: EditorConfig; + metricAggs: AggConfig[]; + state: VisState; +} + +export interface ParamInstance extends ParamInstanceBase { + aggParam: AggParam; + indexedFields: FieldParamType[]; + paramEditor: React.ComponentType>; + value: unknown; +} + +function getAggParamsToRender({ agg, editorConfig, metricAggs, state }: ParamInstanceBase) { + const params = { + basic: [] as ParamInstance[], + advanced: [] as ParamInstance[], + }; + + const paramsToRender = + (agg.type && + agg.type.params + // Filter out, i.e. don't render, any parameter that is hidden via the editor config. + .filter((param: AggParam) => !get(editorConfig, [param.name, 'hidden'], false))) || + []; + + // build collection of agg params components + paramsToRender.forEach((param: AggParam, index: number) => { + let indexedFields: FieldParamType[] = []; + let fields; + + if (agg.schema.hideCustomLabel && param.name === 'customLabel') { + return; + } + // if field param exists, compute allowed fields + if (param.type === 'field') { + const availableFields = (param as FieldParamType).getAvailableFields( + agg.getIndexPattern().fields + ); + fields = aggTypeFieldFilters.filter(availableFields, param.type, agg); + indexedFields = groupAggregationsBy(fields, 'type', 'displayName'); + } + + if (fields && !indexedFields.length && index > 0) { + // don't draw the rest of the options if there are no indexed fields and it's an extra param (index > 0). + return; + } + + const type = param.advanced ? 'advanced' : 'basic'; + + // show params with an editor component + if (param.editorComponent) { + params[type].push({ + agg, + aggParam: param, + editorConfig, + indexedFields, + paramEditor: param.editorComponent, + metricAggs, + state, + value: agg.params[param.name], + } as ParamInstance); + } + }); + + return params; +} + +function getError(agg: AggConfig, aggIsTooLow: boolean) { + const errors = []; + if (aggIsTooLow) { + errors.push( + i18n.translate('common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage', { + defaultMessage: '"{schema}" aggs must run before all other buckets!', + values: { schema: agg.schema.title }, + }) + ); + } + + return errors; +} + +function getAggTypeOptions(agg: AggConfig, indexPattern: IndexPattern, groupName: string) { + const aggTypeOptions = aggTypeFilters.filter(aggTypes.byType[groupName], indexPattern, agg); + return groupAggregationsBy(aggTypeOptions, 'subtype'); +} + +/** + * Calculates a ngModel touched state. + * If an aggregation is not selected, it returns a value of touched agg selector state. + * Else if there are no invalid agg params, it returns false. + * Otherwise it returns true if each invalid param is touched. + * @param aggType Selected aggregation. + * @param aggTypeState State of aggregation selector. + * @param aggParams State of aggregation parameters. + */ +function isInvalidParamsTouched( + aggType: AggType, + aggTypeState: AggTypeState, + aggParams: AggParamsState +) { + if (!aggType) { + return aggTypeState.touched; + } + + const invalidParams = Object.values(aggParams).filter(param => !param.valid); + + if (isEmpty(invalidParams)) { + return false; + } + + return invalidParams.every(param => param.touched); +} + +export { getAggParamsToRender, getError, getAggTypeOptions, isInvalidParamsTouched }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params_state.ts b/src/legacy/ui/public/vis/editors/default/components/agg_params_state.ts new file mode 100644 index 000000000000..3ae53248c8a3 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params_state.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ParamInstance } from './agg_params_helper'; + +export enum AGG_TYPE_ACTION_KEYS { + TOUCHED = 'aggTypeTouched', + VALID = 'aggTypeValid', +} + +export interface AggTypeState { + touched: boolean; + valid: boolean; +} + +export interface AggTypeAction { + type: AGG_TYPE_ACTION_KEYS; + payload: boolean; +} + +function aggTypeReducer(state: AggTypeState, action: AggTypeAction): AggTypeState { + switch (action.type) { + case AGG_TYPE_ACTION_KEYS.TOUCHED: + return { ...state, touched: action.payload }; + case AGG_TYPE_ACTION_KEYS.VALID: + return { ...state, valid: action.payload }; + default: + throw new Error(); + } +} + +export enum AGG_PARAMS_ACTION_KEYS { + TOUCHED = 'aggParamsTouched', + VALID = 'aggParamsValid', + RESET = 'aggParamsReset', +} + +export interface AggParamsItem { + touched: boolean; + valid: boolean; +} + +export interface AggParamsAction { + type: AGG_PARAMS_ACTION_KEYS; + payload?: boolean; + paramName?: string; +} + +export interface AggParamsState { + [key: string]: AggParamsItem; +} + +function aggParamsReducer( + state: AggParamsState, + { type, paramName = '', payload }: AggParamsAction +): AggParamsState { + const targetParam = state[paramName] || { + valid: true, + touched: false, + }; + switch (type) { + case AGG_PARAMS_ACTION_KEYS.TOUCHED: + return { + ...state, + [paramName]: { + ...targetParam, + touched: payload, + }, + } as AggParamsState; + case AGG_PARAMS_ACTION_KEYS.VALID: + return { + ...state, + [paramName]: { + ...targetParam, + valid: payload, + }, + } as AggParamsState; + case AGG_PARAMS_ACTION_KEYS.RESET: + return {}; + default: + throw new Error(); + } +} + +function initAggParamsState(params: ParamInstance[]): AggParamsState { + const state = params.reduce((stateObj: AggParamsState, param: ParamInstance) => { + stateObj[param.aggParam.name] = { + valid: true, + touched: false, + }; + + return stateObj; + }, {}); + + return state; +} + +export { aggTypeReducer, aggParamsReducer, initAggParamsState }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_select.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_select.tsx new file mode 100644 index 000000000000..a607f9127000 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/agg_select.tsx @@ -0,0 +1,148 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, has } from 'lodash'; +import React, { useEffect } from 'react'; + +import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow, EuiLink, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AggType } from 'ui/agg_types'; +import { IndexPattern } from 'ui/index_patterns'; +import { documentationLinks } from '../../../../documentation_links/documentation_links'; +import { ComboBoxGroupedOption } from '../utils'; + +interface DefaultEditorAggSelectProps { + aggError?: string; + aggTypeOptions: AggType[]; + id: string; + indexPattern: IndexPattern; + showValidation: boolean; + isSubAggregation: boolean; + value: AggType; + setValidity: (isValid: boolean) => void; + setValue: (aggType: AggType) => void; + setTouched: () => void; +} + +function DefaultEditorAggSelect({ + aggError, + id, + indexPattern, + value, + setValue, + aggTypeOptions, + showValidation, + isSubAggregation, + setTouched, + setValidity, +}: DefaultEditorAggSelectProps) { + const selectedOptions: ComboBoxGroupedOption[] = value ? [{ label: value.title, value }] : []; + + const label = isSubAggregation ? ( + + ) : ( + + ); + + let aggHelpLink: string | undefined; + if (has(value, 'name')) { + aggHelpLink = get(documentationLinks, ['aggs', value.name]); + } + + const helpLink = value && aggHelpLink && ( + + + + + + ); + + const errors = aggError ? [aggError] : []; + + if (!aggTypeOptions.length) { + errors.push( + i18n.translate('common.ui.vis.defaultEditor.aggSelect.noCompatibleAggsDescription', { + defaultMessage: + 'The index pattern {indexPatternTitle} does not have any aggregatable fields.', + values: { + indexPatternTitle: indexPattern && indexPattern.title, + }, + }) + ); + } + + const isValid = !!value && !errors.length; + + useEffect(() => { + setValidity(isValid); + }, [isValid]); + + useEffect(() => { + if (errors.length) { + setTouched(); + } + }, [errors.length]); + + const onChange = (options: EuiComboBoxOptionProps[]) => { + const selectedOption = get(options, '0.value'); + if (selectedOption) { + setValue(selectedOption); + } + }; + + return ( + + + + ); +} + +export { DefaultEditorAggSelect }; diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_add.tsx b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_add.tsx deleted file mode 100644 index 1d80d330e893..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_add.tsx +++ /dev/null @@ -1,131 +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, { useState } from 'react'; -import { - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiPopoverTitle, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { AggConfig } from 'ui/vis'; -import { Schema } from '../schemas'; -import { AggGroupNames } from '../agg_groups'; - -interface DefaultEditorAggAddProps { - group?: AggConfig[]; - groupName: string; - schemas: Schema[]; - stats: { - max: number; - min: number; - count: number; - deprecate: boolean; - }; - addSchema(schema: Schema): void; -} - -function DefaultEditorAggAdd({ - group = [], - groupName, - schemas, - addSchema, - stats, -}: DefaultEditorAggAddProps) { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const onSelectSchema = (schema: Schema) => { - setIsPopoverOpen(false); - addSchema(schema); - }; - - const addButton = ( - setIsPopoverOpen(!isPopoverOpen)} - > - - - ); - - const groupNameLabel = - groupName === AggGroupNames.Buckets - ? i18n.translate('common.ui.vis.editors.aggAdd.bucketLabel', { defaultMessage: 'bucket' }) - : i18n.translate('common.ui.vis.editors.aggAdd.metricLabel', { defaultMessage: 'metric' }); - - const isSchemaDisabled = (schema: Schema): boolean => { - const count = group.filter(agg => agg.schema.name === schema.name).length; - return count >= schema.max; - }; - - return stats.max > stats.count ? ( - - - setIsPopoverOpen(false)} - > - - {(groupName !== AggGroupNames.Buckets || (!stats.count && !stats.deprecate)) && ( - - )} - {groupName === AggGroupNames.Buckets && stats.count > 0 && !stats.deprecate && ( - - )} - - - !schema.deprecate && ( - onSelectSchema(schema)} - > - {schema.title} - - ) - )} - /> - - - - ) : null; -} - -export { DefaultEditorAggAdd }; diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_param.tsx b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_param.tsx deleted file mode 100644 index e2f2e5b90acb..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_param.tsx +++ /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 React, { useEffect } from 'react'; - -import { AggParams } from '../agg_params'; -import { AggParamEditorProps, AggParamCommonProps } from './default_editor_agg_param_props'; - -interface DefaultEditorAggParamProps extends AggParamCommonProps { - paramEditor: React.ComponentType>; - onChange(aggParams: AggParams, paramName: string, value?: T): void; -} - -function DefaultEditorAggParam(props: DefaultEditorAggParamProps) { - const { agg, aggParam, paramEditor: ParamEditor, onChange, setValidity, ...rest } = props; - - useEffect(() => { - if (aggParam.shouldShow && !aggParam.shouldShow(agg)) { - setValidity(true); - } - }, [agg, agg.params.field]); - - if (aggParam.shouldShow && !aggParam.shouldShow(agg)) { - return null; - } - - return ( - onChange(agg.params, aggParam.name, value)} - {...rest} - /> - ); -} - -export { DefaultEditorAggParam }; diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_param_props.ts b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_param_props.ts deleted file mode 100644 index 30a5445ce14a..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_param_props.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 { AggParam } from '../../../../agg_types'; -import { AggConfig } from '../../../agg_config'; -import { FieldParamType } from '../../../../agg_types/param_types'; -import { EditorConfig } from '../../config/types'; -import { VisState } from '../../../vis'; -import { SubAggParamsProp } from './default_editor_agg_params'; - -// NOTE: we cannot export the interface with export { InterfaceName } -// as there is currently a bug on babel typescript transform plugin for it -// https://github.com/babel/babel/issues/7641 -// - -export interface AggParamCommonProps { - agg: AggConfig; - aggParam: AggParam; - disabled?: boolean; - editorConfig: EditorConfig; - indexedFields?: FieldParamType[]; - showValidation: boolean; - state: VisState; - value?: T; - metricAggs: AggConfig[]; - subAggParams: SubAggParamsProp; - setValidity(isValid: boolean): void; - setTouched(): void; -} - -export interface AggParamEditorProps extends AggParamCommonProps { - setValue(value?: T): void; -} diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params.test.tsx b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params.test.tsx deleted file mode 100644 index 809f0df6f3db..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params.test.tsx +++ /dev/null @@ -1,180 +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 { mount, shallow } from 'enzyme'; -import { IndexPattern } from 'ui/index_patterns'; -import { VisState } from 'ui/vis'; -import { DefaultEditorAggParams, DefaultEditorAggParamsProps } from './default_editor_agg_params'; - -const mockEditorConfig = { - useNormalizedEsInterval: { hidden: false, fixedValue: false }, - interval: { - hidden: false, - help: 'Must be a multiple of rollup configuration interval: 1m', - default: '1m', - timeBase: '1m', - }, -}; - -jest.mock('ui/agg_types', () => ({ - aggTypes: { - byType: { - metrics: [], - buckets: [], - }, - }, -})); -jest.mock('../../config/editor_config_providers', () => ({ - editorConfigProviders: { - getConfigForAgg: jest.fn(() => mockEditorConfig), - }, -})); -jest.mock('./default_editor_agg_params_helper', () => ({ - getAggParamsToRender: jest.fn(() => ({ - basic: [ - { - aggParam: { - displayName: 'Custom label', - name: 'customLabel', - type: 'string', - }, - }, - ], - advanced: [ - { - aggParam: { - advanced: true, - name: 'json', - type: 'json', - }, - }, - ], - })), - getAggTypeOptions: jest.fn(() => []), - getError: jest.fn((agg, aggIsTooLow) => (aggIsTooLow ? ['error'] : [])), - isInvalidParamsTouched: jest.fn(() => false), -})); -jest.mock('./default_editor_agg_select', () => ({ - DefaultEditorAggSelect: () => null, -})); -jest.mock('./default_editor_agg_param', () => ({ - DefaultEditorAggParam: () => null, -})); - -describe('DefaultEditorAggParams component', () => { - let onAggParamsChange: jest.Mock; - let onAggTypeChange: jest.Mock; - let setTouched: jest.Mock; - let setValidity: jest.Mock; - let intervalDeserialize: jest.Mock; - let defaultProps: DefaultEditorAggParamsProps; - - beforeEach(() => { - onAggParamsChange = jest.fn(); - onAggTypeChange = jest.fn(); - setTouched = jest.fn(); - setValidity = jest.fn(); - intervalDeserialize = jest.fn(() => 'deserialized'); - - defaultProps = { - agg: { - type: { - params: [{ name: 'interval', deserialize: intervalDeserialize }], - }, - params: {}, - }, - groupName: 'metrics', - formIsTouched: false, - indexPattern: {} as IndexPattern, - metricAggs: [], - state: {} as VisState, - onAggParamsChange, - onAggTypeChange, - setTouched, - setValidity, - }; - }); - - it('should init with the default set of params', () => { - const comp = shallow(); - - expect(comp).toMatchSnapshot(); - }); - - it('should reset the validity to true when destroyed', () => { - const comp = mount(); - - expect(setValidity).lastCalledWith(false); - - comp.unmount(); - - expect(setValidity).lastCalledWith(true); - }); - - it('should set fixed and default values when editorConfig is defined (works in rollup index)', () => { - mount(); - - expect(onAggParamsChange).toHaveBeenNthCalledWith( - 1, - defaultProps.agg.params, - 'useNormalizedEsInterval', - false - ); - expect(intervalDeserialize).toHaveBeenCalledWith('1m'); - expect(onAggParamsChange).toHaveBeenNthCalledWith( - 2, - defaultProps.agg.params, - 'interval', - 'deserialized' - ); - }); - - it('should call setTouched with false when agg type is changed', () => { - const comp = mount(); - - comp.setProps({ agg: { type: { params: [] } } }); - - expect(setTouched).lastCalledWith(false); - }); - - it('should set the validity when it changed', () => { - const comp = mount(); - - comp.setProps({ aggIsTooLow: true }); - - expect(setValidity).lastCalledWith(false); - - comp.setProps({ aggIsTooLow: false }); - - expect(setValidity).lastCalledWith(true); - }); - - it('should call setTouched when all invalid controls were touched or they are untouched', () => { - const comp = mount(); - - comp.setProps({ aggIsTooLow: true }); - - expect(setTouched).lastCalledWith(true); - - comp.setProps({ aggIsTooLow: false }); - - expect(setTouched).lastCalledWith(false); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params.tsx b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params.tsx deleted file mode 100644 index a5055170e411..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params.tsx +++ /dev/null @@ -1,257 +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, { useReducer, useEffect } from 'react'; -import { EuiForm, EuiAccordion, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { aggTypes, AggType, AggParam } from 'ui/agg_types'; -import { AggConfig, VisState, AggParams } from 'ui/vis'; -import { IndexPattern } from 'ui/index_patterns'; -import { DefaultEditorAggSelect } from './default_editor_agg_select'; -import { DefaultEditorAggParam } from './default_editor_agg_param'; -import { - getAggParamsToRender, - getError, - getAggTypeOptions, - ParamInstance, - isInvalidParamsTouched, -} from './default_editor_agg_params_helper'; -import { - aggTypeReducer, - AGG_TYPE_ACTION_KEYS, - aggParamsReducer, - AGG_PARAMS_ACTION_KEYS, - initAggParamsState, - AggParamsItem, -} from './default_editor_agg_params_state'; -import { editorConfigProviders } from '../../config/editor_config_providers'; -import { FixedParam, TimeIntervalParam, EditorParamConfig } from '../../config/types'; -// TODO: Below import is temporary, use `react-use` lib instead. -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { useUnmount } from '../../../../../../../plugins/kibana_react/public/util/use_unmount'; -import { AggGroupNames } from '../agg_groups'; - -const FIXED_VALUE_PROP = 'fixedValue'; -const DEFAULT_PROP = 'default'; -type EditorParamConfigType = EditorParamConfig & { - [key: string]: unknown; -}; -export interface SubAggParamsProp { - formIsTouched: boolean; - onAggParamsChange: (agg: AggParams, paramName: string, value: unknown) => void; - onAggTypeChange: (agg: AggConfig, aggType: AggType) => void; -} -export interface DefaultEditorAggParamsProps extends SubAggParamsProp { - agg: AggConfig; - aggError?: string | null; - aggIndex?: number; - aggIsTooLow?: boolean; - className?: string; - disabledParams?: string[]; - groupName: string; - indexPattern: IndexPattern; - metricAggs: AggConfig[]; - state: VisState; - setTouched: (isTouched: boolean) => void; - setValidity: (isValid: boolean) => void; -} - -function DefaultEditorAggParams({ - agg, - aggError, - aggIndex = 0, - aggIsTooLow = false, - className, - disabledParams, - groupName, - formIsTouched, - indexPattern, - metricAggs, - state = {} as VisState, - onAggParamsChange, - onAggTypeChange, - setTouched, - setValidity, -}: DefaultEditorAggParamsProps) { - const groupedAggTypeOptions = getAggTypeOptions(agg, indexPattern, groupName); - const errors = getError(agg, aggIsTooLow); - - const editorConfig = editorConfigProviders.getConfigForAgg( - aggTypes.byType[groupName], - indexPattern, - agg - ); - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); - const allParams = [...params.basic, ...params.advanced]; - const [paramsState, onChangeParamsState] = useReducer( - aggParamsReducer, - allParams, - initAggParamsState - ); - const [aggType, onChangeAggType] = useReducer(aggTypeReducer, { touched: false, valid: true }); - - const isFormValid = - !errors.length && - aggType.valid && - Object.entries(paramsState).every(([, paramState]) => paramState.valid); - - const isAllInvalidParamsTouched = - !!errors.length || isInvalidParamsTouched(agg.type, aggType, paramsState); - - // reset validity before component destroyed - useUnmount(() => setValidity(true)); - - useEffect(() => { - Object.entries(editorConfig).forEach(([param, paramConfig]) => { - const paramOptions = agg.type.params.find( - (paramOption: AggParam) => paramOption.name === param - ); - - const hasFixedValue = paramConfig.hasOwnProperty(FIXED_VALUE_PROP); - const hasDefault = paramConfig.hasOwnProperty(DEFAULT_PROP); - // If the parameter has a fixed value in the config, set this value. - // Also for all supported configs we should freeze the editor for this param. - if (hasFixedValue || hasDefault) { - let newValue; - let property = FIXED_VALUE_PROP; - let typedParamConfig: EditorParamConfigType = paramConfig as FixedParam; - - if (hasDefault) { - property = DEFAULT_PROP; - typedParamConfig = paramConfig as TimeIntervalParam; - } - - if (paramOptions && paramOptions.deserialize) { - newValue = paramOptions.deserialize(typedParamConfig[property]); - } else { - newValue = typedParamConfig[property]; - } - onAggParamsChange(agg.params, param, newValue); - } - }); - }, [agg.type]); - - useEffect(() => { - setTouched(false); - }, [agg.type]); - - useEffect(() => { - setValidity(isFormValid); - }, [isFormValid, agg.type]); - - useEffect(() => { - // when all invalid controls were touched or they are untouched - setTouched(isAllInvalidParamsTouched); - }, [isAllInvalidParamsTouched]); - - const renderParam = (paramInstance: ParamInstance, model: AggParamsItem) => { - return ( - { - onChangeParamsState({ - type: AGG_PARAMS_ACTION_KEYS.VALID, - paramName: paramInstance.aggParam.name, - payload: valid, - }); - }} - // setTouched can be called from sub-agg which passes a parameter - setTouched={(isTouched: boolean = true) => { - onChangeParamsState({ - type: AGG_PARAMS_ACTION_KEYS.TOUCHED, - paramName: paramInstance.aggParam.name, - payload: isTouched, - }); - }} - subAggParams={{ - onAggParamsChange, - onAggTypeChange, - formIsTouched, - }} - {...paramInstance} - /> - ); - }; - - return ( - - = 1 && groupName === AggGroupNames.Buckets} - showValidation={formIsTouched || aggType.touched} - setValue={value => { - onAggTypeChange(agg, value); - // reset touched and valid of params - onChangeParamsState({ type: AGG_PARAMS_ACTION_KEYS.RESET }); - }} - setTouched={() => onChangeAggType({ type: AGG_TYPE_ACTION_KEYS.TOUCHED, payload: true })} - setValidity={valid => onChangeAggType({ type: AGG_TYPE_ACTION_KEYS.VALID, payload: valid })} - /> - - {params.basic.map((param: ParamInstance) => { - const model = paramsState[param.aggParam.name] || { - touched: false, - valid: true, - }; - - return renderParam(param, model); - })} - - {params.advanced.length ? ( - <> - - - {params.advanced.map((param: ParamInstance) => { - const model = paramsState[param.aggParam.name] || { - touched: false, - valid: true, - }; - return renderParam(param, model); - })} - - - - ) : null} - - ); -} - -export { DefaultEditorAggParams }; diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params_helper.test.ts b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params_helper.test.ts deleted file mode 100644 index 786ec688bd2b..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params_helper.test.ts +++ /dev/null @@ -1,230 +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 { AggConfig, VisState } from 'ui/vis'; -import { FieldParamType, AggType } from 'ui/agg_types'; -import { IndexPattern } from 'ui/index_patterns'; -import { - getAggParamsToRender, - getError, - getAggTypeOptions, - isInvalidParamsTouched, -} from './default_editor_agg_params_helper'; -import { EditorConfig } from '../../config/types'; - -jest.mock('ui/agg_types', () => ({ - aggTypes: { - byType: { - metrics: [], - buckets: [], - }, - }, -})); -jest.mock('../default_editor_utils', () => ({ - groupAggregationsBy: jest.fn(() => ['indexedFields']), -})); - -describe('DefaultEditorAggParams helpers', () => { - describe('getAggParamsToRender', () => { - let agg: AggConfig; - let editorConfig: EditorConfig; - const state = {} as VisState; - const metricAggs: AggConfig[] = []; - const emptyParams = { - basic: [], - advanced: [], - }; - - it('should not create any param if they do not have editorComponents', () => { - agg = { - type: { - params: [{ name: 'interval' }], - }, - schema: {}, - }; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); - - expect(params).toEqual(emptyParams); - }); - - it('should not create any param if there is no agg type', () => { - agg = {}; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); - - expect(params).toEqual(emptyParams); - }); - - it('should not create a param if it is hidden', () => { - agg = { - type: { - params: [{ name: 'interval' }], - }, - }; - editorConfig = { - interval: { - hidden: true, - }, - }; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); - - expect(params).toEqual(emptyParams); - }); - - it('should skip customLabel param if it is hidden', () => { - agg = { - type: { - params: [{ name: 'customLabel' }], - }, - schema: { - hideCustomLabel: true, - }, - }; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); - - expect(params).toEqual(emptyParams); - }); - - it('should create a basic params field and orderBy', () => { - const filterFieldTypes = ['number', 'boolean', 'date']; - agg = { - type: { - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes, - getAvailableFields: jest.fn((fields: FieldParamType[]) => - fields.filter(({ type }) => filterFieldTypes.includes(type)) - ), - editorComponent: jest.fn(), - }, - { - name: 'orderBy', - editorComponent: jest.fn(), - }, - ], - }, - schema: {}, - getIndexPattern: jest.fn(() => ({ - fields: [{ name: '@timestamp', type: 'date' }, { name: 'geo_desc', type: 'string' }], - })), - params: { - orderBy: 'orderBy', - field: 'field', - }, - }; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); - - expect(params).toEqual({ - basic: [ - { - agg, - aggParam: agg.type.params[0], - editorConfig, - indexedFields: ['indexedFields'], - paramEditor: agg.type.params[0].editorComponent, - metricAggs, - state, - value: agg.params.field, - }, - { - agg, - aggParam: agg.type.params[1], - editorConfig, - indexedFields: [], - paramEditor: agg.type.params[1].editorComponent, - metricAggs, - state, - value: agg.params.orderBy, - }, - ], - advanced: [], - }); - expect(agg.getIndexPattern).toBeCalledTimes(1); - }); - }); - - describe('getError', () => { - it('should not have any errors', () => { - const errors = getError({ schema: { title: 'Split series' } }, false); - - expect(errors).toEqual([]); - }); - - it('should push an error if an agg is too low', () => { - const errors = getError({ schema: { title: 'Split series' } }, true); - - expect(errors).toEqual(['"Split series" aggs must run before all other buckets!']); - }); - - it('should push an error if a schema is deprecated', () => { - const errors = getError({ schema: { title: 'Split series', deprecate: true } }, false); - - expect(errors).toEqual(['"Split series" has been deprecated.']); - }); - }); - - describe('getAggTypeOptions', () => { - it('should return agg type options grouped by subtype', () => { - const indexPattern = {} as IndexPattern; - const aggs = getAggTypeOptions({}, indexPattern, 'metrics'); - - expect(aggs).toEqual(['indexedFields']); - }); - }); - - describe('isInvalidParamsTouched', () => { - let aggType: AggType; - const aggTypeState = { - touched: false, - valid: true, - }; - const aggParams = { - orderBy: { - touched: true, - valid: true, - }, - orderAgg: { - touched: true, - valid: true, - }, - }; - - it('should return aggTypeState touched if there is no aggType', () => { - const isTouched = isInvalidParamsTouched(aggType, aggTypeState, aggParams); - - expect(isTouched).toBe(aggTypeState.touched); - }); - - it('should return false if there is no invalid params', () => { - aggType = 'type'; - const isTouched = isInvalidParamsTouched(aggType, aggTypeState, aggParams); - - expect(isTouched).toBeFalsy(); - }); - - it('should return true if there is an invalid param, but not every still touched', () => { - aggType = 'type'; - aggParams.orderAgg.valid = false; - const isTouched = isInvalidParamsTouched(aggType, aggTypeState, aggParams); - - expect(isTouched).toBeTruthy(); - }); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params_helper.ts b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params_helper.ts deleted file mode 100644 index 5c8acac4e56b..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params_helper.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 { get, isEmpty } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { AggConfig, VisState } from 'ui/vis'; -import { aggTypeFilters } from 'ui/agg_types/filter'; -import { IndexPattern } from 'ui/index_patterns'; -import { aggTypes, AggParam, FieldParamType, AggType } from 'ui/agg_types'; -import { aggTypeFieldFilters } from 'ui/agg_types/param_types/filter'; -import { groupAggregationsBy } from '../default_editor_utils'; -import { EditorConfig } from '../../config/types'; -import { AggTypeState, AggParamsState } from './default_editor_agg_params_state'; -import { AggParamEditorProps } from './default_editor_agg_param_props'; - -interface ParamInstanceBase { - agg: AggConfig; - editorConfig: EditorConfig; - metricAggs: AggConfig[]; - state: VisState; -} - -export interface ParamInstance extends ParamInstanceBase { - aggParam: AggParam; - indexedFields: FieldParamType[]; - paramEditor: React.ComponentType>; - value: unknown; -} - -function getAggParamsToRender({ agg, editorConfig, metricAggs, state }: ParamInstanceBase) { - const params = { - basic: [] as ParamInstance[], - advanced: [] as ParamInstance[], - }; - - const paramsToRender = - (agg.type && - agg.type.params - // Filter out, i.e. don't render, any parameter that is hidden via the editor config. - .filter((param: AggParam) => !get(editorConfig, [param.name, 'hidden'], false))) || - []; - - // build collection of agg params components - paramsToRender.forEach((param: AggParam, index: number) => { - let indexedFields: FieldParamType[] = []; - let fields; - - if (agg.schema.hideCustomLabel && param.name === 'customLabel') { - return; - } - // if field param exists, compute allowed fields - if (param.type === 'field') { - const availableFields = (param as FieldParamType).getAvailableFields( - agg.getIndexPattern().fields - ); - fields = aggTypeFieldFilters.filter(availableFields, param.type, agg); - indexedFields = groupAggregationsBy(fields, 'type', 'displayName'); - } - - if (fields && !indexedFields.length && index > 0) { - // don't draw the rest of the options if there are no indexed fields and it's an extra param (index > 0). - return; - } - - const type = param.advanced ? 'advanced' : 'basic'; - - // show params with an editor component - if (param.editorComponent) { - params[type].push({ - agg, - aggParam: param, - editorConfig, - indexedFields, - paramEditor: param.editorComponent, - metricAggs, - state, - value: agg.params[param.name], - } as ParamInstance); - } - }); - - return params; -} - -function getError(agg: AggConfig, aggIsTooLow: boolean) { - const errors = []; - if (aggIsTooLow) { - errors.push( - i18n.translate('common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage', { - defaultMessage: '"{schema}" aggs must run before all other buckets!', - values: { schema: agg.schema.title }, - }) - ); - } - if (agg.schema.deprecate) { - errors.push( - agg.schema.deprecateMessage - ? agg.schema.deprecateMessage - : i18n.translate('common.ui.vis.editors.aggParams.errors.schemaIsDeprecatedErrorMessage', { - defaultMessage: '"{schema}" has been deprecated.', - values: { schema: agg.schema.title }, - }) - ); - } - - return errors; -} - -function getAggTypeOptions(agg: AggConfig, indexPattern: IndexPattern, groupName: string) { - const aggTypeOptions = aggTypeFilters.filter(aggTypes.byType[groupName], indexPattern, agg); - return groupAggregationsBy(aggTypeOptions, 'subtype'); -} - -/** - * Calculates a ngModel touched state. - * If an aggregation is not selected, it returns a value of touched agg selector state. - * Else if there are no invalid agg params, it returns false. - * Otherwise it returns true if each invalid param is touched. - * @param aggType Selected aggregation. - * @param aggTypeState State of aggregation selector. - * @param aggParams State of aggregation parameters. - */ -function isInvalidParamsTouched( - aggType: AggType, - aggTypeState: AggTypeState, - aggParams: AggParamsState -) { - if (!aggType) { - return aggTypeState.touched; - } - - const invalidParams = Object.values(aggParams).filter(param => !param.valid); - - if (isEmpty(invalidParams)) { - return false; - } - - return invalidParams.every(param => param.touched); -} - -export { getAggParamsToRender, getError, getAggTypeOptions, isInvalidParamsTouched }; diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params_state.ts b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params_state.ts deleted file mode 100644 index 61fabe682071..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_params_state.ts +++ /dev/null @@ -1,114 +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 { ParamInstance } from './default_editor_agg_params_helper'; - -export enum AGG_TYPE_ACTION_KEYS { - TOUCHED = 'aggTypeTouched', - VALID = 'aggTypeValid', -} - -export interface AggTypeState { - touched: boolean; - valid: boolean; -} - -export interface AggTypeAction { - type: AGG_TYPE_ACTION_KEYS; - payload: boolean; -} - -function aggTypeReducer(state: AggTypeState, action: AggTypeAction): AggTypeState { - switch (action.type) { - case AGG_TYPE_ACTION_KEYS.TOUCHED: - return { ...state, touched: action.payload }; - case AGG_TYPE_ACTION_KEYS.VALID: - return { ...state, valid: action.payload }; - default: - throw new Error(); - } -} - -export enum AGG_PARAMS_ACTION_KEYS { - TOUCHED = 'aggParamsTouched', - VALID = 'aggParamsValid', - RESET = 'aggParamsReset', -} - -export interface AggParamsItem { - touched: boolean; - valid: boolean; -} - -export interface AggParamsAction { - type: AGG_PARAMS_ACTION_KEYS; - payload?: boolean; - paramName?: string; -} - -export interface AggParamsState { - [key: string]: AggParamsItem; -} - -function aggParamsReducer( - state: AggParamsState, - { type, paramName = '', payload }: AggParamsAction -): AggParamsState { - const targetParam = state[paramName] || { - valid: true, - touched: false, - }; - switch (type) { - case AGG_PARAMS_ACTION_KEYS.TOUCHED: - return { - ...state, - [paramName]: { - ...targetParam, - touched: payload, - }, - } as AggParamsState; - case AGG_PARAMS_ACTION_KEYS.VALID: - return { - ...state, - [paramName]: { - ...targetParam, - valid: payload, - }, - } as AggParamsState; - case AGG_PARAMS_ACTION_KEYS.RESET: - return {}; - default: - throw new Error(); - } -} - -function initAggParamsState(params: ParamInstance[]): AggParamsState { - const state = params.reduce((stateObj: AggParamsState, param: ParamInstance) => { - stateObj[param.aggParam.name] = { - valid: true, - touched: false, - }; - - return stateObj; - }, {}); - - return state; -} - -export { aggTypeReducer, aggParamsReducer, initAggParamsState }; diff --git a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_select.tsx b/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_select.tsx deleted file mode 100644 index 536bcdb7891e..000000000000 --- a/src/legacy/ui/public/vis/editors/default/components/default_editor_agg_select.tsx +++ /dev/null @@ -1,151 +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, has } from 'lodash'; -import React, { useEffect } from 'react'; - -import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow, EuiLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { AggType } from 'ui/agg_types'; -import { IndexPattern } from 'ui/index_patterns'; -import { documentationLinks } from '../../../../documentation_links/documentation_links'; -import { ComboBoxGroupedOption } from '../default_editor_utils'; - -interface DefaultEditorAggSelectProps { - aggError?: string | null; - aggTypeOptions: AggType[]; - id: string; - indexPattern: IndexPattern; - showValidation: boolean; - isSubAggregation: boolean; - value: AggType; - setValidity: (isValid: boolean) => void; - setValue: (aggType: AggType) => void; - setTouched: () => void; -} - -function DefaultEditorAggSelect({ - aggError, - id, - indexPattern, - value, - setValue, - aggTypeOptions, - showValidation, - isSubAggregation, - setTouched, - setValidity, -}: DefaultEditorAggSelectProps) { - const selectedOptions: ComboBoxGroupedOption[] = value ? [{ label: value.title, value }] : []; - - const label = isSubAggregation ? ( - - ) : ( - - ); - - let aggHelpLink: string | undefined; - if (has(value, 'name')) { - aggHelpLink = get(documentationLinks, ['aggs', value.name]); - } - - const helpLink = value && aggHelpLink && ( - - - - ); - - const errors = aggError ? [aggError] : []; - - if (!aggTypeOptions.length) { - errors.push( - i18n.translate('common.ui.vis.defaultEditor.aggSelect.noCompatibleAggsDescription', { - defaultMessage: - 'The index pattern {indexPatternTitle} does not have any aggregatable fields.', - values: { - indexPatternTitle: indexPattern && indexPattern.title, - }, - }) - ); - } - - const isValid = !!value && !errors.length; - - useEffect(() => { - setValidity(isValid); - }, [isValid]); - - useEffect(() => { - if (errors.length) { - setTouched(); - } - }, [errors.length]); - - const onChange = (options: EuiComboBoxOptionProps[]) => { - const selectedOption = get(options, '0.value'); - if (selectedOption) { - setValue(selectedOption); - } - }; - - return ( - - - - ); -} - -export { DefaultEditorAggSelect }; diff --git a/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx b/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx index fa153d2facea..91294dffe71d 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx @@ -17,7 +17,8 @@ * under the License. */ -import { VisParams, AggParams } from 'ui/vis'; +import { VisParams } from '../../..'; +import { AggParams } from '../agg_params'; export interface AggControlProps { aggParams: AggParams; diff --git a/src/legacy/ui/public/vis/editors/default/controls/agg_controls.js b/src/legacy/ui/public/vis/editors/default/controls/agg_controls.js deleted file mode 100644 index 860c33bc9940..000000000000 --- a/src/legacy/ui/public/vis/editors/default/controls/agg_controls.js +++ /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 { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from '../../../../modules'; -import { AggControlReactWrapper } from './agg_control_react_wrapper'; - -uiModules - .get('app/visualize') - .directive('visAggControlReactWrapper', reactDirective => reactDirective(wrapInI18nContext(AggControlReactWrapper), [ - ['aggParams', { watchDepth: 'collection' }], - ['editorStateParams', { watchDepth: 'collection' }], - ['component', { wrapApply: false }], - 'setValue' - ])); diff --git a/src/legacy/ui/public/vis/editors/default/default.js b/src/legacy/ui/public/vis/editors/default/default.js index 6fdbbd1d150a..da8ab50b7805 100644 --- a/src/legacy/ui/public/vis/editors/default/default.js +++ b/src/legacy/ui/public/vis/editors/default/default.js @@ -18,6 +18,7 @@ */ import 'ui/angular-bootstrap'; +import './fancy_forms'; import './sidebar'; import { i18n } from '@kbn/i18n'; import './vis_options'; diff --git a/src/legacy/ui/public/vis/editors/default/index.ts b/src/legacy/ui/public/vis/editors/default/index.ts index 590249667b74..2439715d9211 100644 --- a/src/legacy/ui/public/vis/editors/default/index.ts +++ b/src/legacy/ui/public/vis/editors/default/index.ts @@ -17,5 +17,8 @@ * under the License. */ -export { AggParamEditorProps } from './components/default_editor_agg_param_props'; -export { DefaultEditorAggParams } from './components/default_editor_agg_params'; +export { AggParamEditorProps } from './components/agg_param_props'; +export { DefaultEditorAggParams, SubAggParamsProp } from './components/agg_params'; +export * from './vis_options_props'; +export * from './utils'; +export * from './agg_groups'; diff --git a/src/legacy/ui/public/vis/editors/default/keyboard_move.js b/src/legacy/ui/public/vis/editors/default/keyboard_move.js deleted file mode 100644 index 9f5b1eefa0ce..000000000000 --- a/src/legacy/ui/public/vis/editors/default/keyboard_move.js +++ /dev/null @@ -1,75 +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 keyboardMove directive can be attached to elements, that can receive keydown events. - * It will call the passed callback function and pass the direction in which an - * arrow key was pressed to the callback (as the argument with the name `direction`). - * The passed value will be one of `Direction.up` or `Direction.down`, which can be - * imported to compare against those values. The directive will also make sure, that - * the pressed button will get the focus back (e.g. if it was lost due to a ng-repeat - * reordering). - * - * Usage example: - * - * - * - * import { Direction } from './keyboard_move'; - * function onMoved(dir) { - * if (dir === Direction.up) { - * // moved up - * } else if (dir === Direction.down) { - * // moved down - * } - * } - */ -import { uiModules } from '../../../modules'; -import { keyCodes } from '@elastic/eui'; - -export const Direction = { - up: 'up', - down: 'down' -}; - -const directionMapping = { - [keyCodes.UP]: Direction.up, - [keyCodes.DOWN]: Direction.down -}; - -uiModules.get('kibana') - .directive('keyboardMove', ($parse, $timeout) => ({ - restrict: 'A', - link(scope, el, attr) { - const callbackFn = $parse(attr.keyboardMove); - el.keydown((ev) => { - if (ev.which in directionMapping) { - ev.preventDefault(); - const direction = directionMapping[ev.which]; - scope.$apply(() => callbackFn(scope, { direction })); - // Keep focus on that element, even though it might be attached somewhere - // else in the DOM (e.g. because it has a new position in an ng-repeat). - $timeout(() => el.focus()); - } - }); - - scope.$on('$destroy', () => { - el.off('keydown'); - }); - } - })); diff --git a/src/legacy/ui/public/vis/editors/default/schemas.d.ts b/src/legacy/ui/public/vis/editors/default/schemas.d.ts index ae4aae9fe106..faf6120cfb89 100644 --- a/src/legacy/ui/public/vis/editors/default/schemas.d.ts +++ b/src/legacy/ui/public/vis/editors/default/schemas.d.ts @@ -22,7 +22,6 @@ import { AggGroupNames } from './agg_groups'; export interface Schema { aggFilter: string | string[]; - deprecate: boolean; editor: boolean | string; group: AggGroupNames; max: number; diff --git a/src/legacy/ui/public/vis/editors/default/schemas.js b/src/legacy/ui/public/vis/editors/default/schemas.js index 4c3da3bb336c..313fdfd19a28 100644 --- a/src/legacy/ui/public/vis/editors/default/schemas.js +++ b/src/legacy/ui/public/vis/editors/default/schemas.js @@ -51,7 +51,6 @@ class Schemas { aggFilter: '*', editor: false, params: [], - deprecate: false }); // convert the params into a params registry diff --git a/src/legacy/ui/public/vis/editors/default/sidebar.html b/src/legacy/ui/public/vis/editors/default/sidebar.html index 77a930c94a46..14e9bd46c376 100644 --- a/src/legacy/ui/public/vis/editors/default/sidebar.html +++ b/src/legacy/ui/public/vis/editors/default/sidebar.html @@ -151,10 +151,24 @@
    - - + +
    - +
    @@ -166,6 +180,7 @@ ui-state="uiState" visualize-editor="visualizeEditor" editor="tab.editor" + on-agg-params-change="onAggParamsChange" >
    diff --git a/src/legacy/ui/public/vis/editors/default/sidebar.js b/src/legacy/ui/public/vis/editors/default/sidebar.js index a335880bcf91..30e42d195769 100644 --- a/src/legacy/ui/public/vis/editors/default/sidebar.js +++ b/src/legacy/ui/public/vis/editors/default/sidebar.js @@ -23,30 +23,79 @@ import './vis_options'; import 'ui/directives/css_truncate'; import { uiModules } from '../../../modules'; import sidebarTemplate from './sidebar.html'; +import { move } from '../../../utils/collection'; +import { AggConfig } from '../../agg_config'; -uiModules - .get('app/visualize') - .directive('visEditorSidebar', function () { - return { - restrict: 'E', - template: sidebarTemplate, - scope: true, - controllerAs: 'sidebar', - controller: function ($scope) { - $scope.$watch('vis.type', (visType) => { - if (visType) { - this.showData = visType.schemas.buckets || visType.schemas.metrics; - if (_.has(visType, 'editorConfig.optionTabs')) { - const activeTabs = visType.editorConfig.optionTabs.filter((tab) => { - return _.get(tab, 'active', false); - }); - if (activeTabs.length > 0) { - this.section = activeTabs[0].name; - } +uiModules.get('app/visualize').directive('visEditorSidebar', function () { + return { + restrict: 'E', + template: sidebarTemplate, + scope: true, + require: '?^ngModel', + controllerAs: 'sidebar', + controller: function ($scope) { + $scope.$watch('vis.type', visType => { + if (visType) { + this.showData = visType.schemas.buckets || visType.schemas.metrics; + if (_.has(visType, 'editorConfig.optionTabs')) { + const activeTabs = visType.editorConfig.optionTabs.filter(tab => { + return _.get(tab, 'active', false); + }); + if (activeTabs.length > 0) { + this.section = activeTabs[0].name; } - this.section = this.section || (this.showData ? 'data' : _.get(visType, 'editorConfig.optionTabs[0].name')); } + this.section = + this.section || + (this.showData ? 'data' : _.get(visType, 'editorConfig.optionTabs[0].name')); + } + }); + + $scope.onAggTypeChange = (agg, value) => { + if (agg.type !== value) { + agg.type = value; + } + }; + + $scope.onAggParamsChange = (params, paramName, value) => { + if (params[paramName] !== value) { + params[paramName] = value; + } + }; + + $scope.addSchema = function (schema) { + const aggConfig = new AggConfig($scope.state.aggs, { + schema, + id: AggConfig.nextId($scope.state.aggs), + }); + aggConfig.brandNew = true; + + $scope.state.aggs.push(aggConfig); + }; + + $scope.removeAgg = function (agg) { + const aggs = $scope.state.aggs; + const index = aggs.indexOf(agg); + + if (index === -1) { + return; + } + + aggs.splice(index, 1); + }; + + $scope.onToggleEnableAgg = (agg, isEnable) => { + agg.enabled = isEnable; + }; + + $scope.reorderAggs = (group) => { + //the aggs have been reordered in [group] and we need + //to apply that ordering to [vis.aggs] + const indexOffset = $scope.state.aggs.indexOf(group[0]); + _.forEach(group, (agg, index) => { + move($scope.state.aggs, agg, indexOffset + index); }); - } - }; - }); + }; + }, + }; +}); diff --git a/src/legacy/ui/public/vis/editors/default/utils.test.tsx b/src/legacy/ui/public/vis/editors/default/utils.test.tsx new file mode 100644 index 000000000000..3fc1edd4b121 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/utils.test.tsx @@ -0,0 +1,195 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { groupAggregationsBy } from './utils'; +import { AggGroupNames } from './agg_groups'; + +const aggs = [ + { + title: 'Count', + type: AggGroupNames.Metrics, + subtype: 'Metric Aggregations', + }, + { + title: 'Average', + type: AggGroupNames.Metrics, + subtype: 'Metric Aggregations', + }, + { + title: 'Cumulative Sum', + type: AggGroupNames.Metrics, + subtype: 'Parent Pipeline Aggregations', + }, + { + title: 'Min Bucket', + type: AggGroupNames.Metrics, + subtype: 'Parent Pipeline Aggregations', + }, + { + title: 'Sub string agg', + type: 'string', + subtype: 'Sub-String aggregations', + }, + { + title: 'String agg', + type: 'string', + subtype: 'String aggregations', + }, +]; + +describe('Default Editor groupAggregationsBy', () => { + it('should return aggs grouped by default type field', () => { + const groupedAggs = [ + { + label: AggGroupNames.Metrics, + options: [ + { + label: 'Average', + value: { + title: 'Average', + type: AggGroupNames.Metrics, + subtype: 'Metric Aggregations', + }, + }, + { + label: 'Count', + value: { + title: 'Count', + type: AggGroupNames.Metrics, + subtype: 'Metric Aggregations', + }, + }, + { + label: 'Cumulative Sum', + value: { + title: 'Cumulative Sum', + type: AggGroupNames.Metrics, + subtype: 'Parent Pipeline Aggregations', + }, + }, + + { + label: 'Min Bucket', + value: { + title: 'Min Bucket', + type: AggGroupNames.Metrics, + subtype: 'Parent Pipeline Aggregations', + }, + }, + ], + }, + { + label: 'string', + options: [ + { + label: 'String agg', + value: { + title: 'String agg', + type: 'string', + subtype: 'String aggregations', + }, + }, + { + label: 'Sub string agg', + value: { + title: 'Sub string agg', + type: 'string', + subtype: 'Sub-String aggregations', + }, + }, + ], + }, + ]; + expect(groupAggregationsBy(aggs)).toEqual(groupedAggs); + }); + it('should return aggs grouped by subtype field', () => { + const groupedAggs = [ + { + label: 'Metric Aggregations', + options: [ + { + label: 'Average', + value: { + title: 'Average', + type: AggGroupNames.Metrics, + subtype: 'Metric Aggregations', + }, + }, + { + label: 'Count', + value: { + title: 'Count', + type: AggGroupNames.Metrics, + subtype: 'Metric Aggregations', + }, + }, + ], + }, + { + label: 'Parent Pipeline Aggregations', + options: [ + { + label: 'Cumulative Sum', + value: { + title: 'Cumulative Sum', + type: AggGroupNames.Metrics, + subtype: 'Parent Pipeline Aggregations', + }, + }, + + { + label: 'Min Bucket', + value: { + title: 'Min Bucket', + type: AggGroupNames.Metrics, + subtype: 'Parent Pipeline Aggregations', + }, + }, + ], + }, + { + label: 'String aggregations', + options: [ + { + label: 'String agg', + value: { + title: 'String agg', + type: 'string', + subtype: 'String aggregations', + }, + }, + ], + }, + { + label: 'Sub-String aggregations', + options: [ + { + label: 'Sub string agg', + value: { + title: 'Sub string agg', + type: 'string', + subtype: 'Sub-String aggregations', + }, + }, + ], + }, + ]; + expect(groupAggregationsBy(aggs, 'subtype')).toEqual(groupedAggs); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/default_editor_utils.tsx b/src/legacy/ui/public/vis/editors/default/utils.tsx similarity index 100% rename from src/legacy/ui/public/vis/editors/default/default_editor_utils.tsx rename to src/legacy/ui/public/vis/editors/default/utils.tsx diff --git a/src/legacy/ui/public/vis/editors/default/vis_options.html b/src/legacy/ui/public/vis/editors/default/vis_options.html deleted file mode 100644 index 51a4c296b5b7..000000000000 --- a/src/legacy/ui/public/vis/editors/default/vis_options.html +++ /dev/null @@ -1,4 +0,0 @@ - -
    diff --git a/src/legacy/ui/public/vis/editors/default/vis_options.js b/src/legacy/ui/public/vis/editors/default/vis_options.js index fe1bc3a0f61e..0f4548e11556 100644 --- a/src/legacy/ui/public/vis/editors/default/vis_options.js +++ b/src/legacy/ui/public/vis/editors/default/vis_options.js @@ -17,12 +17,9 @@ * under the License. */ -import _ from 'lodash'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; +import { wrapInI18nContext } from 'ui/i18n'; import { uiModules } from '../../../modules'; -import visOptionsTemplate from './vis_options.html'; -import { I18nContext } from 'ui/i18n'; +import { VisOptionsReactWrapper } from './vis_options_react_wrapper'; /** * This directive sort of "transcludes" in whatever template you pass in via the `editor` attribute. @@ -32,10 +29,15 @@ import { I18nContext } from 'ui/i18n'; uiModules .get('app/visualize') + .directive('visOptionsReactWrapper', reactDirective => reactDirective(wrapInI18nContext(VisOptionsReactWrapper), [ + ['component', { wrapApply: false }], + ['stateParams', { watchDepth: 'collection' }], + ['vis', { watchDepth: 'collection' }], + ['setValue', { watchDepth: 'reference' }], + ])) .directive('visEditorVisOptions', function ($compile) { return { restrict: 'E', - template: visOptionsTemplate, scope: { vis: '=', visData: '=', @@ -43,45 +45,22 @@ uiModules editor: '=', visualizeEditor: '=', editorState: '=', + onAggParamsChange: '=', }, link: function ($scope, $el) { - const $optionContainer = $el.find('[data-visualization-options]'); + $scope.setValue = (paramName, value) => + $scope.onAggParamsChange($scope.editorState.params, paramName, value); - const reactOptionsComponent = typeof $scope.editor !== 'string'; - const stageEditorParams = (params) => { - $scope.editorState.params = _.cloneDeep(params); - $scope.$apply(); - }; - const renderReactComponent = () => { - const Component = $scope.editor; - render( - - - , $el[0]); - }; - // Bind the `editor` template with the scope. - if (reactOptionsComponent) { - renderReactComponent(); - } else { - const $editor = $compile($scope.editor)($scope); - $optionContainer.append($editor); - } - - $scope.$watchGroup(['visData', 'visualizeEditor', 'editorState.params'], () => { - if (reactOptionsComponent) { - renderReactComponent(); - } - }); - - $scope.$watch('vis.type.schemas.all.length', function (len) { - $scope.alwaysShowOptions = len === 0; - }); - - $el.on('$destroy', () => { - if (reactOptionsComponent) { - unmountComponentAtNode($el[0]); - } - }); + const comp = typeof $scope.editor === 'string' ? + $scope.editor : + ` + `; + const $editor = $compile(comp)($scope); + $el.append($editor); } }; }); diff --git a/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx b/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx new file mode 100644 index 000000000000..e82a0fdc0892 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Vis } from './../..'; + +export interface VisOptionsProps { + stateParams: VisParamType; + vis: Vis; + setValue(paramName: T, value: VisParamType[T]): void; +} diff --git a/src/legacy/ui/public/vis/editors/default/vis_options_react_wrapper.tsx b/src/legacy/ui/public/vis/editors/default/vis_options_react_wrapper.tsx new file mode 100644 index 000000000000..d214abecb9c0 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/vis_options_react_wrapper.tsx @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { VisOptionsProps } from './vis_options_props'; + +interface VisOptionsReactWrapperProps extends VisOptionsProps { + component: React.ComponentType; +} + +function VisOptionsReactWrapper({ component: Component, ...rest }: VisOptionsReactWrapperProps) { + return ; +} + +export { VisOptionsReactWrapper }; diff --git a/src/legacy/ui/public/vis/editors/index.js b/src/legacy/ui/public/vis/editors/index.js deleted file mode 100644 index d3ba8dd04578..000000000000 --- a/src/legacy/ui/public/vis/editors/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 { EditorOptionsGroup } from './components'; diff --git a/src/legacy/ui/public/vis/index.d.ts b/src/legacy/ui/public/vis/index.d.ts index 31ae827bff37..e5d9131bd0fb 100644 --- a/src/legacy/ui/public/vis/index.d.ts +++ b/src/legacy/ui/public/vis/index.d.ts @@ -18,8 +18,6 @@ */ export { AggConfig } from './agg_config'; -export { AggParams } from './editors/default/agg_params'; -export { AggParamEditorProps } from './editors/default/components/default_editor_agg_param_props'; export { Vis, VisProvider, VisParams, VisState } from './vis'; export { VisualizationController, VisType } from './vis_types/vis_type'; export * from './request_handlers'; diff --git a/src/legacy/ui/public/vis/map/service_settings.d.ts b/src/legacy/ui/public/vis/map/service_settings.d.ts new file mode 100644 index 000000000000..8c391f42419c --- /dev/null +++ b/src/legacy/ui/public/vis/map/service_settings.d.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 TmsLayer { + id: string; + origin: string; + minZoom: string; + maxZoom: number; + attribution: string; +} + +export interface ServiceSettings { + getTMSServices(): Promise; +} diff --git a/src/legacy/ui/public/vis/request_handlers/courier.js b/src/legacy/ui/public/vis/request_handlers/courier.js index 80f842534019..5cf5bf005d0b 100644 --- a/src/legacy/ui/public/vis/request_handlers/courier.js +++ b/src/legacy/ui/public/vis/request_handlers/courier.js @@ -42,7 +42,6 @@ const CourierRequestHandlerProvider = function () { inspectorAdapters, queryFilter }) { - // Create a new search source that inherits the original search source // but has the appropriate timeRange applied via a filter. // This is a temporary solution until we properly pass down all required diff --git a/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts b/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts index f2475065a869..f6768007503c 100644 --- a/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts +++ b/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts @@ -24,12 +24,12 @@ import { SearchSource } from '../../courier'; import { QueryFilter } from '../../filter_manager/query_filter'; import { Adapters } from '../../inspector/types'; import { PersistedState } from '../../persisted_state'; -import { AggConfigs } from '../agg_configs'; +import { AggConfig } from '../agg_config'; import { Vis } from '../vis'; export interface RequestHandlerParams { searchSource: SearchSource; - aggs: AggConfigs; + aggs: AggConfig[]; timeRange?: TimeRange; query?: Query; filters?: Filter[]; diff --git a/src/legacy/ui/public/vis/vis.d.ts b/src/legacy/ui/public/vis/vis.d.ts index 9e6107ed7594..22881a6fda18 100644 --- a/src/legacy/ui/public/vis/vis.d.ts +++ b/src/legacy/ui/public/vis/vis.d.ts @@ -18,6 +18,7 @@ */ import { VisType } from './vis_types/vis_type'; +import { AggConfigs } from './agg_configs'; export interface Vis { type: VisType; @@ -39,5 +40,5 @@ export interface VisState { title: string; type: VisType; params: VisParams; - aggs: any[]; + aggs: AggConfigs; } diff --git a/src/legacy/ui/public/vis/vis.js b/src/legacy/ui/public/vis/vis.js index dc537cd000d3..7b7b31104355 100644 --- a/src/legacy/ui/public/vis/vis.js +++ b/src/legacy/ui/public/vis/vis.js @@ -38,7 +38,7 @@ import { SearchSourceProvider } from '../courier/search_source'; import { SavedObjectsClientProvider } from '../saved_objects'; import { timefilter } from '../timefilter'; -import '../bind'; +import '../directives/bind'; export function VisProvider(Private, indexPatterns, getAppState) { const visTypes = Private(VisTypesRegistryProvider); diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_type.js b/src/legacy/ui/public/vis/vis_types/vislib_vis_type.js index 2e664fadf9c0..cb1297a0a50f 100644 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_type.js +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_type.js @@ -17,7 +17,6 @@ * under the License. */ -import 'plugins/kbn_vislib_vis_types/controls/vislib_basic_options'; import 'plugins/kbn_vislib_vis_types/controls/point_series_options'; import 'plugins/kbn_vislib_vis_types/controls/line_interpolation_option'; import 'plugins/kbn_vislib_vis_types/controls/heatmap_options'; diff --git a/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js b/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js index f48e2014385b..aedb2f016b50 100644 --- a/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js +++ b/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js @@ -36,8 +36,7 @@ import { dispatchRenderComplete } from '../../../render_complete'; import { PipelineDataLoader } from '../pipeline_data_loader'; import { VisualizeDataLoader } from '../visualize_data_loader'; import { PersistedState } from '../../../persisted_state'; -import { DataAdapter } from '../../../inspector/adapters/data'; -import { RequestAdapter } from '../../../inspector/adapters/request'; +import { DataAdapter, RequestAdapter } from '../../../inspector/adapters'; describe('visualize loader', () => { diff --git a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts index b56a63c4c092..d6a5ede713c6 100644 --- a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts +++ b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts @@ -20,7 +20,7 @@ import { mockDataLoaderFetch, timefilter } from './embedded_visualize_handler.te // @ts-ignore import MockState from '../../../../../fixtures/mock_state'; -import { RequestHandlerParams, Vis } from '../../vis'; +import { RequestHandlerParams, Vis, AggConfig } from '../../vis'; import { VisResponseData } from './types'; import { Inspector } from '../../inspector'; @@ -49,7 +49,7 @@ describe('EmbeddedVisualizeHandler', () => { jest.clearAllMocks(); dataLoaderParams = { - aggs: [], + aggs: [] as AggConfig[], filters: undefined, forceFetch: false, inspectorAdapters: {}, @@ -157,7 +157,7 @@ describe('EmbeddedVisualizeHandler', () => { }); it('should call dataLoader.render with updated filters', () => { - const params = { filters: [{ foo: 'bar' }] }; + const params = { filters: [{ meta: { disabled: false } }] }; handler.update(params); jest.runAllTimers(); expect(mockDataLoaderFetch).toHaveBeenCalledWith({ ...dataLoaderParams, ...params }); diff --git a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts index 431c5ae9be6a..774ccb4d2a06 100644 --- a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts +++ b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts @@ -18,7 +18,7 @@ */ import { EventEmitter } from 'events'; -import { debounce, forEach, get } from 'lodash'; +import { debounce, forEach, get, isEqual } from 'lodash'; import * as Rx from 'rxjs'; import { share } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; @@ -38,6 +38,7 @@ import { VisFiltersProvider } from '../../vis/vis_filters'; import { PipelineDataLoader } from './pipeline_data_loader'; import { visualizationLoader } from './visualization_loader'; import { VisualizeDataLoader } from './visualize_data_loader'; +import { onlyDisabledFiltersChanged } from '../../../../core_plugins/data/public'; import { DataAdapter, RequestAdapter } from '../../inspector/adapters'; @@ -236,15 +237,21 @@ export class EmbeddedVisualizeHandler { } let fetchRequired = false; - if (params.hasOwnProperty('timeRange')) { + if ( + params.hasOwnProperty('timeRange') && + !isEqual(this.dataLoaderParams.timeRange, params.timeRange) + ) { fetchRequired = true; this.dataLoaderParams.timeRange = params.timeRange; } - if (params.hasOwnProperty('filters')) { + if ( + params.hasOwnProperty('filters') && + !onlyDisabledFiltersChanged(this.dataLoaderParams.filters, params.filters) + ) { fetchRequired = true; this.dataLoaderParams.filters = params.filters; } - if (params.hasOwnProperty('query')) { + if (params.hasOwnProperty('query') && !isEqual(this.dataLoaderParams.query, params.query)) { fetchRequired = true; this.dataLoaderParams.query = params.query; } diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts index d98872cb3402..d98feac9a022 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts @@ -51,6 +51,11 @@ describe('visualize loader pipeline helpers: build pipeline', () => { const actual = prepareJson('foo', { well: `hello 'hi'`, there: { friend: true } }); expect(actual).toBe(expected); }); + + it('returns empty string if data is undefined', () => { + const actual = prepareJson('foo', undefined); + expect(actual).toBe(''); + }); }); describe('prepareString', () => { @@ -65,6 +70,11 @@ describe('visualize loader pipeline helpers: build pipeline', () => { const actual = prepareString('foo', `'bar'`); expect(actual).toBe(expected); }); + + it('returns empty string if data is undefined', () => { + const actual = prepareString('foo', undefined); + expect(actual).toBe(''); + }); }); describe('buildPipelineVisFunction', () => { diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts index 38a53b7078d4..83bb25f26e7d 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts @@ -188,7 +188,7 @@ export const getSchemas = (vis: Vis, timeRange?: any): Schemas => { return schemas; }; -export const prepareJson = (variable: string, data: object): string => { +export const prepareJson = (variable: string, data?: object): string => { if (data === undefined) { return ''; } diff --git a/src/plugins/data/common/expressions/create_error.ts b/src/plugins/data/common/expressions/create_error.ts index 78a12cb07c66..cee288e5e1b3 100644 --- a/src/plugins/data/common/expressions/create_error.ts +++ b/src/plugins/data/common/expressions/create_error.ts @@ -22,5 +22,6 @@ export const createError = (err: any) => ({ error: { stack: process.env.NODE_ENV === 'production' ? undefined : err.stack, message: typeof err === 'string' ? err : err.message, + name: (err && err.name) || 'Error', }, }); diff --git a/src/plugins/data/common/expressions/interpreter_provider.ts b/src/plugins/data/common/expressions/interpreter_provider.ts index ed26701de5d8..cb025e22131c 100644 --- a/src/plugins/data/common/expressions/interpreter_provider.ts +++ b/src/plugins/data/common/expressions/interpreter_provider.ts @@ -71,8 +71,16 @@ export function interpreterProvider(config: any) { // if something failed, just return the failure if (getType(newContext) === 'error') return newContext; + // if execution was aborted return error + if (handlers.abortSignal && handlers.abortSignal.aborted) { + return createError({ + message: 'The expression was aborted.', + name: 'AbortError', + }); + } + // Continue re-invoking chain until it's empty - return await invokeChain(chain, newContext); + return invokeChain(chain, newContext); } catch (e) { // Everything that throws from a function will hit this // The interpreter should *never* fail. It should always return a `{type: error}` on failure diff --git a/src/plugins/es_ui_shared/public/request/index.ts b/src/plugins/es_ui_shared/public/request/index.ts new file mode 100644 index 000000000000..a19005c0191a --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { + SendRequestConfig, + SendRequestResponse, + UseRequestConfig, + sendRequest, + useRequest, +} from './request'; diff --git a/src/plugins/es_ui_shared/public/request/request.test.js b/src/plugins/es_ui_shared/public/request/request.test.js new file mode 100644 index 000000000000..60b05a8dc8a2 --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/request.test.js @@ -0,0 +1,254 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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)); + +describe('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; + + 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 }); + }); + + 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 undefined when the request is successful', async () => { + initUseRequest({ ...successRequest }); + await wait(50); + expect(hook.isLoading).toBe(false); + expect(hook.error).toBeUndefined(); + }); + }); + + 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 undefined when the request fails', async () => { + initUseRequest({ ...errorRequest }); + await wait(50); + expect(hook.isLoading).toBe(false); + expect(hook.data).toBeUndefined(); + }); + }); + }); + + 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 new file mode 100644 index 000000000000..168ad8e2f378 --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/request.ts @@ -0,0 +1,154 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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: string; + body?: any; +} + +export interface SendRequestResponse { + data: any; + error: Error; +} + +export interface UseRequestConfig extends SendRequestConfig { + pollIntervalMs?: number; + initialData?: any; + deserializer?: (data: any) => any; +} + +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 }; + } catch (e) { + return { + error: e.response ? e.response : e, + }; + } +}; + +export const useRequest = ( + httpClient: ng.IHttpService, + { + path, + method, + body, + pollIntervalMs, + initialData, + deserializer = (data: any): any => data, + }: UseRequestConfig +) => { + // 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; + } + + 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(); + }; + + 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/inspector/README.md b/src/plugins/inspector/README.md new file mode 100644 index 000000000000..e56db65cb90c --- /dev/null +++ b/src/plugins/inspector/README.md @@ -0,0 +1,122 @@ +# Inspector + +The inspector is a contextual tool to gain insights into different elements +in Kibana, e.g. visualizations. It has the form of a flyout panel. + +## Inspector Views + +The "Inspector Panel" can have multiple so called "Inspector Views" inside of it. +These views are used to gain different information into the element you are inspecting. +There is a request inspector view to gain information in the requests done for this +element or a data inspector view to inspect the underlying data. Whether or not +a specific view is available depends on the used adapters. + +## Inspector Adapters + +Since the Inspector panel itself is not tied to a specific type of elements (visualizations, +saved searches, etc.), everything you need to open the inspector is a collection +of so called inspector adapters. A single adapter can be any type of JavaScript class. + +Most likely an adapter offers some kind of logging capabilities for the element, that +uses it e.g. the request adapter allows element (like visualizations) to log requests +they make. + +The corresponding inspector view will then use the information inside the adapter +to present the data in the panel. That concept allows different types of elements +to use the Inspector panel, while they can use completely or partial different adapters +and inspector views than other elements. + +For example a visualization could provide the request and data adapter while a saved +search could only provide the request adapter and a Vega visualization could additionally +provide a Vega adapter. + +There is no 1 to 1 relationship between adapters and views. An adapter could be used +by multiple views and a view can use data from multiple adapters. It's up to the +view to decide whether or not it wants to be shown for a given adapters list. + +## Develop custom inspectors + +You can extend the inspector panel by adding custom inspector views and inspector +adapters via a plugin. + +### Develop inspector views + +To develop custom inspector views you can define your +inspector view as follows: + +```js +import React from 'react'; +import { viewRegistry } from 'ui/inspector'; + +function MyInspectorComponent(props) { + // props.adapters is the object of all adapters and may vary depending + // on who and where this inspector was opened. You should check for all + // adapters you need, in the below shouldShow method, before accessing + // them here. + return ( + <> + My custom view.... + + ); +} + +const MyLittleInspectorView = { + // Title shown to select this view + title: 'Display Name', + // An icon id from the EUI icon list + icon: 'iconName', + // An order to sort the views (lower means first) + order: 10, + // An additional helptext, that wil + help: `And additional help text, that will be shown in the inspector help.`, + shouldShow(adapters) { + // Only show if `someAdapter` is available. Make sure to check for + // all adapters that you want to access in your view later on and + // any additional condition you want to be true to be shown. + return adapters.someAdapter; + }, + // A React component, that will be used for rendering + component: MyInspectorComponent +}; +``` + +Then register your view in *setup* life-cycle with `inspector` plugin. + +```ts +class MyPlugin extends Plugin { + setup(core, { inspector }) { + inspector.registerView(MyLittleInspectorView); + } +} +``` + +### Develop custom adapters + +An inspector adapter is just a plain JavaScript class, that can e.g. be attached +to custom visualization types, so an inspector view can show additional information for this +visualization. + +To add additional adapters to your visualization type, use the `inspectorAdapters.custom` +object when defining the visualization type: + +```js +class MyCustomInspectorAdapter { + // .... +} + +// inside your visualization type description (usually passed to VisFactory.create...Type) +{ + // ... + inspectorAdapters: { + custom: { + someAdapter: MyCustomInspectorAdapter + } + } +} +``` + +An instance of MyCustomInspectorAdapter will now be available on each visualization +of that type and can be accessed via `vis.API.inspectorAdapters.someInspector`. + +Custom inspector views can now check for the presence of `adapters.someAdapter` +in their `shouldShow` method and use this adapter in their component. diff --git a/src/plugins/inspector/kibana.json b/src/plugins/inspector/kibana.json new file mode 100644 index 000000000000..39d3ff65eed5 --- /dev/null +++ b/src/plugins/inspector/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "inspector", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/legacy/ui/public/inspector/adapters/data/data_adapter.ts b/src/plugins/inspector/public/adapters/data/data_adapter.ts similarity index 100% rename from src/legacy/ui/public/inspector/adapters/data/data_adapter.ts rename to src/plugins/inspector/public/adapters/data/data_adapter.ts diff --git a/src/legacy/ui/public/inspector/adapters/data/data_adapters.test.ts b/src/plugins/inspector/public/adapters/data/data_adapters.test.ts similarity index 100% rename from src/legacy/ui/public/inspector/adapters/data/data_adapters.test.ts rename to src/plugins/inspector/public/adapters/data/data_adapters.test.ts diff --git a/src/legacy/ui/public/inspector/adapters/data/formatted_data.ts b/src/plugins/inspector/public/adapters/data/formatted_data.ts similarity index 100% rename from src/legacy/ui/public/inspector/adapters/data/formatted_data.ts rename to src/plugins/inspector/public/adapters/data/formatted_data.ts diff --git a/src/legacy/ui/public/inspector/adapters/data/index.ts b/src/plugins/inspector/public/adapters/data/index.ts similarity index 100% rename from src/legacy/ui/public/inspector/adapters/data/index.ts rename to src/plugins/inspector/public/adapters/data/index.ts diff --git a/src/plugins/inspector/public/adapters/index.ts b/src/plugins/inspector/public/adapters/index.ts new file mode 100644 index 000000000000..8e1979ab3327 --- /dev/null +++ b/src/plugins/inspector/public/adapters/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 { DataAdapter, FormattedData } from './data'; +export { RequestAdapter, RequestStatus } from './request'; diff --git a/src/legacy/ui/public/inspector/adapters/request/index.ts b/src/plugins/inspector/public/adapters/request/index.ts similarity index 100% rename from src/legacy/ui/public/inspector/adapters/request/index.ts rename to src/plugins/inspector/public/adapters/request/index.ts diff --git a/src/legacy/ui/public/inspector/adapters/request/request_adapter.test.ts b/src/plugins/inspector/public/adapters/request/request_adapter.test.ts similarity index 100% rename from src/legacy/ui/public/inspector/adapters/request/request_adapter.test.ts rename to src/plugins/inspector/public/adapters/request/request_adapter.test.ts diff --git a/src/legacy/ui/public/inspector/adapters/request/request_adapter.ts b/src/plugins/inspector/public/adapters/request/request_adapter.ts similarity index 100% rename from src/legacy/ui/public/inspector/adapters/request/request_adapter.ts rename to src/plugins/inspector/public/adapters/request/request_adapter.ts diff --git a/src/legacy/ui/public/inspector/adapters/request/request_responder.ts b/src/plugins/inspector/public/adapters/request/request_responder.ts similarity index 93% rename from src/legacy/ui/public/inspector/adapters/request/request_responder.ts rename to src/plugins/inspector/public/adapters/request/request_responder.ts index 31aa56030d87..36ae6a147b99 100644 --- a/src/legacy/ui/public/inspector/adapters/request/request_responder.ts +++ b/src/plugins/inspector/public/adapters/request/request_responder.ts @@ -48,11 +48,11 @@ export class RequestResponder { const startDate = new Date(this.request.startTime); this.request.stats.requestTimestamp = { - label: i18n.translate('common.ui.inspector.reqTimestampKey', { + label: i18n.translate('inspector.reqTimestampKey', { defaultMessage: 'Request timestamp', }), value: startDate.toISOString(), - description: i18n.translate('common.ui.inspector.reqTimestampDescription', { + description: i18n.translate('inspector.reqTimestampDescription', { defaultMessage: 'Time when the start of the request has been logged', }), }; diff --git a/src/legacy/ui/public/inspector/adapters/request/types.ts b/src/plugins/inspector/public/adapters/request/types.ts similarity index 100% rename from src/legacy/ui/public/inspector/adapters/request/types.ts rename to src/plugins/inspector/public/adapters/request/types.ts diff --git a/src/plugins/inspector/public/index.ts b/src/plugins/inspector/public/index.ts new file mode 100644 index 000000000000..ad0c9b77e915 --- /dev/null +++ b/src/plugins/inspector/public/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { PluginInitializerContext } from '../../../core/public'; +import { InspectorPublicPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new InspectorPublicPlugin(initializerContext); +} + +export { InspectorPublicPlugin as Plugin, Setup, Start } from './plugin'; +export * from './types'; diff --git a/src/plugins/inspector/public/mocks.ts b/src/plugins/inspector/public/mocks.ts new file mode 100644 index 000000000000..0e605b1dd306 --- /dev/null +++ b/src/plugins/inspector/public/mocks.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 as PluginSetup, Start as PluginStart } from '.'; +import { InspectorViewRegistry } from './view_registry'; +import { plugin as pluginInitializer } from '.'; +// eslint-disable-next-line +import { coreMock } from '../../../core/public/mocks'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +const createSetupContract = (): Setup => { + const views = new InspectorViewRegistry(); + + const setupContract: Setup = { + registerView: jest.fn(views.register.bind(views)), + + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { + views, + }, + }; + return setupContract; +}; + +const createStartContract = (): Start => { + const startContract: Start = { + isAvailable: jest.fn(), + open: jest.fn(), + }; + + const openResult = { + onClose: Promise.resolve(undefined), + close: jest.fn(() => Promise.resolve(undefined)), + } as ReturnType; + startContract.open.mockImplementation(() => openResult); + + return startContract; +}; + +const createPlugin = async () => { + const pluginInitializerContext = coreMock.createPluginInitializerContext(); + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const plugin = pluginInitializer(pluginInitializerContext); + const setup = await plugin.setup(coreSetup); + + return { + pluginInitializerContext, + coreSetup, + coreStart, + plugin, + setup, + doStart: async () => await plugin.start(coreStart), + }; +}; + +export const inspectorPluginMock = { + createSetupContract, + createStartContract, + createPlugin, +}; diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx new file mode 100644 index 000000000000..53a7adde9b3a --- /dev/null +++ b/src/plugins/inspector/public/plugin.tsx @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { i18n } from '@kbn/i18n'; +import * as React from 'react'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { InspectorViewRegistry } from './view_registry'; +import { Adapters, InspectorOptions, InspectorSession } from './types'; +import { InspectorPanel } from './ui/inspector_panel'; + +export interface Setup { + registerView: InspectorViewRegistry['register']; + + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { + views: InspectorViewRegistry; + }; +} + +export interface Start { + /** + * Checks if a inspector panel could be shown based on the passed adapters. + * + * @param {object} adapters - An object of adapters. This should be the same + * you would pass into `open`. + * @returns {boolean} True, if a call to `open` with the same adapters + * would have shown the inspector panel, false otherwise. + */ + isAvailable: (adapters?: Adapters) => boolean; + + /** + * Opens the inspector panel for the given adapters and close any previously opened + * inspector panel. The previously panel will be closed also if no new panel will be + * opened (e.g. because of the passed adapters no view is available). You can use + * {@link InspectorSession#close} on the return value to close that opened panel again. + * + * @param {object} adapters - An object of adapters for which you want to show + * the inspector panel. + * @param {InspectorOptions} options - Options that configure the inspector. See InspectorOptions type. + * @return {InspectorSession} The session instance for the opened inspector. + * @throws {Error} + */ + open: (adapters: Adapters, options?: InspectorOptions) => InspectorSession; +} + +export class InspectorPublicPlugin implements Plugin { + views: InspectorViewRegistry | undefined; + + constructor(initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup) { + this.views = new InspectorViewRegistry(); + + return { + registerView: this.views!.register.bind(this.views), + + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { + views: this.views, + }, + }; + } + + public start(core: CoreStart) { + const isAvailable: Start['isAvailable'] = adapters => + this.views!.getVisible(adapters).length > 0; + + const closeButtonLabel = i18n.translate('inspector.closeButton', { + defaultMessage: 'Close Inspector', + }); + + const open: Start['open'] = (adapters, options = {}) => { + const views = this.views!.getVisible(adapters); + + // Don't open inspector if there are no views available for the passed adapters + if (!views || views.length === 0) { + throw new Error(`Tried to open an inspector without views being available. + Make sure to call Inspector.isAvailable() with the same adapters before to check + if an inspector can be shown.`); + } + + return core.overlays.openFlyout( + , + { + 'data-test-subj': 'inspectorPanel', + closeButtonAriaLabel: closeButtonLabel, + } + ); + }; + + return { + isAvailable, + open, + }; + } + + public stop() {} +} diff --git a/src/plugins/inspector/public/test/is_available.test.ts b/src/plugins/inspector/public/test/is_available.test.ts new file mode 100644 index 000000000000..1aeffd68a9f3 --- /dev/null +++ b/src/plugins/inspector/public/test/is_available.test.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 { inspectorPluginMock } from '../mocks'; +import { DataAdapter } from '../adapters/data/data_adapter'; +import { RequestAdapter } from '../adapters/request/request_adapter'; + +const adapter1 = new DataAdapter(); +const adapter2 = new RequestAdapter(); + +describe('inspector', () => { + describe('isAvailable()', () => { + it('should return false if no view would be available', async () => { + const { doStart } = await inspectorPluginMock.createPlugin(); + const start = await doStart(); + expect(start.isAvailable({ adapter1 })).toBe(false); + }); + + it('should return true if views would be available, false otherwise', async () => { + const { setup, doStart } = await inspectorPluginMock.createPlugin(); + + setup.registerView({ + title: 'title', + help: 'help', + shouldShow(adapters: any) { + return 'adapter1' in adapters; + }, + } as any); + + const start = await doStart(); + + expect(start.isAvailable({ adapter1 })).toBe(true); + expect(start.isAvailable({ adapter2 })).toBe(false); + }); + }); +}); diff --git a/src/plugins/inspector/public/test/open.test.ts b/src/plugins/inspector/public/test/open.test.ts new file mode 100644 index 000000000000..94cf161bb11c --- /dev/null +++ b/src/plugins/inspector/public/test/open.test.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { inspectorPluginMock } from '../mocks'; + +describe('inspector', () => { + describe('open()', () => { + it('should throw an error if no views available', async () => { + const { doStart } = await inspectorPluginMock.createPlugin(); + const start = await doStart(); + expect(() => start.open({})).toThrow(); + }); + }); +}); diff --git a/src/plugins/inspector/public/types.ts b/src/plugins/inspector/public/types.ts new file mode 100644 index 000000000000..5c3fd770c28d --- /dev/null +++ b/src/plugins/inspector/public/types.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { OverlayRef } from '../../../core/public'; + +/** + * The interface that the adapters used to open an inspector have to fullfill. + */ +export interface Adapters { + [key: string]: any; +} + +/** + * The props interface that a custom inspector view component, that will be passed + * to {@link InspectorViewDescription#component}, must use. + */ +export interface InspectorViewProps { + /** + * Adapters used to open the inspector. + */ + adapters: Adapters; + /** + * The title that the inspector is currently using e.g. a visualization name. + */ + title: string; +} + +/** + * An object describing an inspector view. + * @typedef {object} InspectorViewDescription + * @property {string} title - The title that will be used to present that view. + * @property {string} icon - An icon name to present this view. Must match an EUI icon. + * @property {React.ComponentType} component - The actual React component to render that view. + * @property {number} [order=9000] - An order for this view. Views are ordered from lower + * order values to higher order values in the UI. + * @property {string} [help=''] - An help text for this view, that gives a brief description + * of this view. + * @property {viewShouldShowFunc} [shouldShow] - A function, that determines whether + * this view should be visible for a given collection of adapters. If not specified + * the view will always be visible. + */ +export interface InspectorViewDescription { + component: React.ComponentType; + help?: string; + order?: number; + shouldShow?: (adapters: Adapters) => boolean; + title: string; +} + +/** + * Options that can be specified when opening the inspector. + * @property {string} title - An optional title, that will be shown in the header + * of the inspector. Can be used to give more context about what is being inspected. + */ +export interface InspectorOptions { + title?: string; +} + +export type InspectorSession = OverlayRef; diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap new file mode 100644 index 000000000000..843fd78b24be --- /dev/null +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -0,0 +1,350 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InspectorPanel should render as expected 1`] = ` + + +
    + +
    + +
    + +

    + Inspector +

    +
    +
    +
    + +
    + + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="inspectorViewChooser" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +

    + View 1 +

    +
    +
    +
    +
    +
    +`; diff --git a/src/plugins/inspector/public/ui/inspector_panel.test.tsx b/src/plugins/inspector/public/ui/inspector_panel.test.tsx new file mode 100644 index 000000000000..c482b6fa8033 --- /dev/null +++ b/src/plugins/inspector/public/ui/inspector_panel.test.tsx @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { InspectorPanel } from './inspector_panel'; +import { Adapters, InspectorViewDescription } from '../types'; + +describe('InspectorPanel', () => { + let adapters: Adapters; + let views: InspectorViewDescription[]; + + beforeEach(() => { + adapters = { + foodapter: { + foo() { + return 42; + }, + }, + bardapter: {}, + }; + views = [ + { + title: 'View 1', + order: 200, + component: () =>

    View 1

    , + }, + { + title: 'Foo View', + order: 100, + component: () =>

    Foo view

    , + shouldShow(adapters2: Adapters) { + return adapters2.foodapter; + }, + }, + { + title: 'Never', + order: 200, + component: () => null, + shouldShow() { + return false; + }, + }, + ]; + }); + + it('should render as expected', () => { + const component = mountWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('should not allow updating adapters', () => { + const component = mountWithIntl(); + adapters.notAllowed = {}; + expect(() => component.setProps({ adapters })).toThrow(); + }); +}); diff --git a/src/plugins/inspector/public/ui/inspector_panel.tsx b/src/plugins/inspector/public/ui/inspector_panel.tsx new file mode 100644 index 000000000000..953bf7e1f073 --- /dev/null +++ b/src/plugins/inspector/public/ui/inspector_panel.tsx @@ -0,0 +1,129 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { i18n } from '@kbn/i18n'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { EuiFlexGroup, EuiFlexItem, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; +import { Adapters, InspectorViewDescription } from '../types'; +import { InspectorViewChooser } from './inspector_view_chooser'; + +function hasAdaptersChanged(oldAdapters: Adapters, newAdapters: Adapters) { + return ( + Object.keys(oldAdapters).length !== Object.keys(newAdapters).length || + Object.keys(oldAdapters).some(key => oldAdapters[key] !== newAdapters[key]) + ); +} + +const inspectorTitle = i18n.translate('inspector.title', { + defaultMessage: 'Inspector', +}); + +interface InspectorPanelProps { + adapters: Adapters; + title?: string; + views: InspectorViewDescription[]; +} + +interface InspectorPanelState { + selectedView: InspectorViewDescription; + views: InspectorViewDescription[]; + adapters: Adapters; +} + +export class InspectorPanel extends Component { + static defaultProps = { + title: inspectorTitle, + }; + + static propTypes = { + adapters: PropTypes.object.isRequired, + views: (props: InspectorPanelProps, propName: string, componentName: string) => { + if (!Array.isArray(props.views) || props.views.length < 1) { + throw new Error( + `${propName} prop must be an array of at least one element in ${componentName}.` + ); + } + }, + title: PropTypes.string, + }; + + state: InspectorPanelState = { + selectedView: this.props.views[0], + views: this.props.views, + // Clone adapters array so we can validate that this prop never change + adapters: { ...this.props.adapters }, + }; + + static getDerivedStateFromProps(nextProps: InspectorPanelProps, prevState: InspectorPanelState) { + if (hasAdaptersChanged(prevState.adapters, nextProps.adapters)) { + throw new Error('Adapters are not allowed to be changed on an open InspectorPanel.'); + } + const selectedViewMustChange = + nextProps.views !== prevState.views && !nextProps.views.includes(prevState.selectedView); + return { + views: nextProps.views, + selectedView: selectedViewMustChange ? nextProps.views[0] : prevState.selectedView, + }; + } + + onViewSelected = (view: InspectorViewDescription) => { + if (view !== this.state.selectedView) { + this.setState({ + selectedView: view, + }); + } + }; + + renderSelectedPanel() { + return ( + + ); + } + + render() { + const { views, title } = this.props; + const { selectedView } = this.state; + + return ( + + + + + +

    {title}

    +
    +
    + + + +
    +
    + {this.renderSelectedPanel()} +
    + ); + } +} diff --git a/src/plugins/inspector/public/ui/inspector_view_chooser.tsx b/src/plugins/inspector/public/ui/inspector_view_chooser.tsx new file mode 100644 index 000000000000..ce6027ad383c --- /dev/null +++ b/src/plugins/inspector/public/ui/inspector_view_chooser.tsx @@ -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 { FormattedMessage } from '@kbn/i18n/react'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiToolTip, +} from '@elastic/eui'; +import { InspectorViewDescription } from '../types'; + +interface Props { + views: InspectorViewDescription[]; + onViewSelected: (view: InspectorViewDescription) => void; + selectedView: InspectorViewDescription; +} + +interface State { + isSelectorOpen: boolean; +} + +export class InspectorViewChooser extends Component { + static propTypes = { + views: PropTypes.array.isRequired, + onViewSelected: PropTypes.func.isRequired, + selectedView: PropTypes.object.isRequired, + }; + + state: State = { + isSelectorOpen: false, + }; + + toggleSelector = () => { + this.setState(prev => ({ + isSelectorOpen: !prev.isSelectorOpen, + })); + }; + + closeSelector = () => { + this.setState({ + isSelectorOpen: false, + }); + }; + + renderView = (view: InspectorViewDescription, index: number) => { + return ( + { + this.props.onViewSelected(view); + this.closeSelector(); + }} + toolTipContent={view.help} + toolTipPosition="left" + data-test-subj={`inspectorViewChooser${view.title}`} + > + {view.title} + + ); + }; + + renderViewButton() { + return ( + + + + ); + } + + renderSingleView() { + return ( + + + + ); + } + + render() { + const { views } = this.props; + + if (views.length < 2) { + return this.renderSingleView(); + } + + const triggerButton = this.renderViewButton(); + + return ( + + + + ); + } +} diff --git a/src/legacy/ui/public/inspector/view_registry.test.ts b/src/plugins/inspector/public/view_registry.test.ts similarity index 96% rename from src/legacy/ui/public/inspector/view_registry.test.ts rename to src/plugins/inspector/public/view_registry.test.ts index e073c6431892..830ee107213f 100644 --- a/src/legacy/ui/public/inspector/view_registry.test.ts +++ b/src/plugins/inspector/public/view_registry.test.ts @@ -17,7 +17,8 @@ * under the License. */ -import { InspectorViewDescription, InspectorViewRegistry } from './view_registry'; +import { InspectorViewRegistry } from './view_registry'; +import { InspectorViewDescription } from './types'; import { Adapters } from './types'; diff --git a/src/plugins/inspector/public/view_registry.ts b/src/plugins/inspector/public/view_registry.ts new file mode 100644 index 000000000000..4a35baf1f3ef --- /dev/null +++ b/src/plugins/inspector/public/view_registry.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 { EventEmitter } from 'events'; +import { Adapters, InspectorViewDescription } from './types'; + +/** + * @callback viewShouldShowFunc + * @param {object} adapters - A list of adapters to check whether or not this view + * should be shown for. + * @returns {boolean} true - if this view should be shown for the given adapters. + */ + +/** + * A registry that will hold inspector views. + */ +export class InspectorViewRegistry extends EventEmitter { + private views: InspectorViewDescription[] = []; + + /** + * Register a new inspector view to the registry. Check the README.md in the + * inspector directory for more information of the object format to register + * here. This will also emit a 'change' event on the registry itself. + * + * @param {InspectorViewDescription} view - The view description to add to the registry. + */ + public register(view: InspectorViewDescription): void { + if (!view) { + return; + } + this.views.push(view); + // Keep registry sorted by the order property + this.views.sort((a, b) => (a.order || Number.MAX_VALUE) - (b.order || Number.MAX_VALUE)); + this.emit('change'); + } + + /** + * Retrieve all views currently registered with the registry. + * @returns {InspectorViewDescription[]} A by `order` sorted list of all registered + * inspector views. + */ + public getAll(): InspectorViewDescription[] { + return this.views; + } + + /** + * Retrieve all registered views, that want to be visible for the specified adapters. + * @param {object} adapters - an adapter configuration + * @returns {InspectorViewDescription[]} All inespector view descriptions visible + * for the specific adapters. + */ + public getVisible(adapters?: Adapters): InspectorViewDescription[] { + if (!adapters) { + return []; + } + return this.views.filter(view => !view.shouldShow || view.shouldShow(adapters)); + } +} diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index f14b0b5fb2d4..706a8050a0ad 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -221,13 +221,17 @@ export function createTestServers({ await es.start(); if (['gold', 'trial'].includes(license)) { - await setupUsers(log, esTestConfig.getUrlParts().port, [ - ...usersToBeAdded, - // user elastic - esTestConfig.getUrlParts(), - // user kibana - kbnTestConfig.getUrlParts(), - ]); + await setupUsers({ + log, + esPort: esTestConfig.getUrlParts().port, + updates: [ + ...usersToBeAdded, + // user elastic + esTestConfig.getUrlParts(), + // user kibana + kbnTestConfig.getUrlParts(), + ], + }); // Override provided configs, we know what the elastic user is now kbnSettings.elasticsearch = { diff --git a/src/test_utils/public/stub_index_pattern.js b/src/test_utils/public/stub_index_pattern.js index e7afef2a94a4..0fe07cab3632 100644 --- a/src/test_utils/public/stub_index_pattern.js +++ b/src/test_utils/public/stub_index_pattern.js @@ -18,13 +18,14 @@ */ import sinon from 'sinon'; -import { IndexPattern } from 'ui/index_patterns/_index_pattern'; -import { getRoutes } from 'ui/index_patterns/get_routes'; -import { formatHitProvider } from 'ui/index_patterns/_format_hit'; -import { getComputedFields } from 'ui/index_patterns/_get_computed_fields'; +import { + IndexPattern, + FieldList, + getRoutes, + formatHitProvider, + flattenHitWrapper, +} from 'ui/index_patterns'; import { fieldFormats } from 'ui/registry/field_formats'; -import { flattenHitWrapper } from 'ui/index_patterns/_flatten_hit'; -import { FieldList } from 'ui/index_patterns/_field_list'; export default function () { @@ -36,12 +37,13 @@ export default function () { this.isTimeBased = () => Boolean(this.timeFieldName); this.getNonScriptedFields = sinon.spy(IndexPattern.prototype.getNonScriptedFields); this.getScriptedFields = sinon.spy(IndexPattern.prototype.getScriptedFields); + this.getFieldByName = sinon.spy(IndexPattern.prototype.getFieldByName); this.getSourceFiltering = sinon.stub(); this.metaFields = ['_id', '_type', '_source']; this.fieldFormatMap = {}; this.routes = getRoutes(); - this.getComputedFields = getComputedFields.bind(this); + this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this); this.flattenHit = flattenHitWrapper(this, this.metaFields); this.formatHit = formatHitProvider(this, fieldFormats.getDefaultInstance('string')); this.fieldsFetcher = { apiClient: { baseUrl: '' } }; diff --git a/tasks/config/karma.js b/tasks/config/karma.js index a59692f1f405..2e1fc114d951 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -28,7 +28,7 @@ module.exports = function (grunt) { if (grunt.option('browser')) { return grunt.option('browser'); } - if (process.env.TEST_BROWSER_HEADLESS) { + if (process.env.TEST_BROWSER_HEADLESS === '1') { return 'Chrome_Headless'; } return 'Chrome'; diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.js index 3058f497a442..544429921508 100644 --- a/test/api_integration/apis/saved_objects/export.js +++ b/test/api_integration/apis/saved_objects/export.js @@ -139,7 +139,7 @@ export default function ({ getService }) { statusCode: 400, error: 'Bad Request', message: 'child "type" fails because ["type" at position 0 fails because ' + - '["0" must be one of [config, index-pattern, visualization, search, dashboard, url]]]', + '["0" must be one of [config, dashboard, index-pattern, search, url, visualization]]]', validation: { source: 'payload', keys: ['type.0'], diff --git a/test/api_integration/config.js b/test/api_integration/config.js index 443dc2ab7a68..d14630f932bf 100644 --- a/test/api_integration/config.js +++ b/test/api_integration/config.js @@ -17,11 +17,7 @@ * under the License. */ -import { - KibanaSupertestProvider, - ElasticsearchSupertestProvider, - ChanceProvider, -} from './services'; +import { services } from './services'; export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); @@ -31,14 +27,7 @@ export default async function ({ readConfigFile }) { testFiles: [ require.resolve('./apis'), ], - services: { - es: commonConfig.get('services.es'), - esArchiver: commonConfig.get('services.esArchiver'), - retry: commonConfig.get('services.retry'), - supertest: KibanaSupertestProvider, - esSupertest: ElasticsearchSupertestProvider, - chance: ChanceProvider, - }, + services, servers: commonConfig.get('servers'), junit: { reportName: 'API Integration Tests' diff --git a/test/api_integration/services/index.js b/test/api_integration/services/index.js deleted file mode 100644 index 2d363d1ac552..000000000000 --- a/test/api_integration/services/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 { KibanaSupertestProvider, ElasticsearchSupertestProvider } from './supertest'; -export { ChanceProvider } from './chance'; diff --git a/test/api_integration/services/index.ts b/test/api_integration/services/index.ts new file mode 100644 index 000000000000..d0fcd94a6a20 --- /dev/null +++ b/test/api_integration/services/index.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { services as commonServices } from '../../common/services'; + +// @ts-ignore not TS yet +import { KibanaSupertestProvider, ElasticsearchSupertestProvider } from './supertest'; + +// @ts-ignore not TS yet +import { ChanceProvider } from './chance'; + +export const services = { + es: commonServices.es, + esArchiver: commonServices.esArchiver, + retry: commonServices.retry, + supertest: KibanaSupertestProvider, + esSupertest: ElasticsearchSupertestProvider, + chance: ChanceProvider, +}; diff --git a/test/functional/README.md b/test/functional/README.md new file mode 100644 index 000000000000..6da3ccfa2c6e --- /dev/null +++ b/test/functional/README.md @@ -0,0 +1,3 @@ +# Kibana Functional Testing + +See our [Functional Testing Guide](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html) diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index 2c85996b31c8..26882ec8a52a 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -46,13 +46,11 @@ export default function ({ getService, getPageObjects }) { it('displays predessors - anchor - successors in right order ', async function () { await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_TYPE, 'AU_x3-TaGFA8no6Qj999Z'); - const table = await docTable.getTable(); - const rows = await docTable.getBodyRows(table); - const actualRowsText = await Promise.all(rows.map(row => row.getVisibleText())); + const actualRowsText = await docTable.getRowsText(); const expectedRowsText = [ - 'Sep 18, 2019 @ 06:50:13.000000000\n-2', - 'Sep 18, 2019 @ 06:50:12.999999999\n-3', - 'Sep 19, 2015 @ 06:50:13.000100001\n1' + 'Sep 18, 2019 @ 06:50:13.000000000-2', + 'Sep 18, 2019 @ 06:50:12.999999999-3', + 'Sep 19, 2015 @ 06:50:13.0001000011' ]; expect(actualRowsText).to.eql(expectedRowsText); }); @@ -61,19 +59,17 @@ export default function ({ getService, getPageObjects }) { await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_TYPE, 'AU_x3-TaGFA8no6Qjisd'); await PageObjects.context.clickPredecessorLoadMoreButton(); await PageObjects.context.clickSuccessorLoadMoreButton(); - const table = await docTable.getTable(); - const rows = await docTable.getBodyRows(table); - const actualRowsText = await Promise.all(rows.map(row => row.getVisibleText())); + const actualRowsText = await docTable.getRowsText(); const expectedRowsText = [ - 'Sep 22, 2019 @ 23:50:13.253123345\n5', - 'Sep 18, 2019 @ 06:50:13.000000104\n4', - 'Sep 18, 2019 @ 06:50:13.000000103\n2', - 'Sep 18, 2019 @ 06:50:13.000000102\n1', - 'Sep 18, 2019 @ 06:50:13.000000101\n0', - 'Sep 18, 2019 @ 06:50:13.000000001\n-1', - 'Sep 18, 2019 @ 06:50:13.000000000\n-2', - 'Sep 18, 2019 @ 06:50:12.999999999\n-3', - 'Sep 19, 2015 @ 06:50:13.000100001\n1' + 'Sep 22, 2019 @ 23:50:13.2531233455', + 'Sep 18, 2019 @ 06:50:13.0000001044', + 'Sep 18, 2019 @ 06:50:13.0000001032', + 'Sep 18, 2019 @ 06:50:13.0000001021', + 'Sep 18, 2019 @ 06:50:13.0000001010', + 'Sep 18, 2019 @ 06:50:13.000000001-1', + 'Sep 18, 2019 @ 06:50:13.000000000-2', + 'Sep 18, 2019 @ 06:50:12.999999999-3', + 'Sep 19, 2015 @ 06:50:13.0001000011' ]; expect(actualRowsText).to.eql(expectedRowsText); diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index 344d773214dc..147d0e74d1c9 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -45,40 +45,26 @@ export default function ({ getService, getPageObjects }) { }); it('should open the context view with the selected document as anchor', async function () { - const discoverDocTable = await docTable.getTable(); - const firstRow = (await docTable.getBodyRows(discoverDocTable))[0]; - // get the timestamp of the first row - const firstTimestamp = await (await docTable.getFields(firstRow))[0] - .getVisibleText(); + const firstTimestamp = (await docTable.getFields())[0][0]; // navigate to the context view - await (await docTable.getRowExpandToggle(firstRow)).click(); - const firstDetailsRow = (await docTable.getDetailsRows(discoverDocTable))[0]; - await (await docTable.getRowActions(firstDetailsRow))[0].click(); + await docTable.clickRowToggle({ rowIndex: 0 }); + await (await docTable.getRowActions({ rowIndex: 0 }))[0].click(); // check the anchor timestamp in the context view await retry.try(async () => { - const contextDocTable = await docTable.getTable(); - const anchorRow = await docTable.getAnchorRow(contextDocTable); - const anchorTimestamp = await (await docTable.getFields(anchorRow))[0] - .getVisibleText(); + const anchorTimestamp = (await docTable.getFields({ isAnchorRow: true }))[0][0]; expect(anchorTimestamp).to.equal(firstTimestamp); }); }); it('should open the context view with the same columns', async function () { - const table = await docTable.getTable(); - await retry.try(async () => { - const headerFields = await docTable.getHeaderFields(table); - const columnNames = await Promise.all(headerFields.map((headerField) => ( - headerField.getVisibleText() - ))); - expect(columnNames).to.eql([ - 'Time', - ...TEST_COLUMN_NAMES, - ]); - }); + const columnNames = await docTable.getHeaderFields(); + expect(columnNames).to.eql([ + 'Time', + ...TEST_COLUMN_NAMES, + ]); }); it('should open the context view with the filters disabled', async function () { diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js index 101588e23396..32332ecc4c5f 100644 --- a/test/functional/apps/context/_filters.js +++ b/test/functional/apps/context/_filters.js @@ -29,6 +29,8 @@ const TEST_COLUMN_NAMES = ['extension', 'geo.src']; export default function ({ getService, getPageObjects }) { const docTable = getService('docTable'); const filterBar = getService('filterBar'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'context']); describe('context filters', function contextSize() { @@ -38,47 +40,40 @@ export default function ({ getService, getPageObjects }) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/39927 - it.skip('should be addable via expanded doc table rows', async function () { - const table = await docTable.getTable(); - const anchorRow = await docTable.getAnchorRow(table); - - await docTable.toggleRowExpanded(anchorRow); + it('should be addable via expanded doc table rows', async function () { + await docTable.toggleRowExpanded({ isAnchorRow: true }); - const anchorDetailsRow = await docTable.getAnchorDetailsRow(table); + const anchorDetailsRow = await docTable.getAnchorDetailsRow(); await docTable.addInclusiveFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - await docTable.toggleRowExpanded(anchorRow); - - expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true)).to.be(true); + await docTable.toggleRowExpanded({ isAnchorRow: true }); - const rows = await docTable.getBodyRows(table); - const hasOnlyFilteredRows = ( - await Promise.all(rows.map( - async (row) => await (await docTable.getFields(row))[2].getVisibleText() - )) - ).every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); - expect(hasOnlyFilteredRows).to.be(true); + await retry.try(async () => { + expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true)).to.be(true); + const fields = await docTable.getFields(); + const hasOnlyFilteredRows = fields + .map(row => row[2]) + .every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); + expect(hasOnlyFilteredRows).to.be(true); + }); }); it('should be toggleable via the filter bar', async function () { - const table = await docTable.getTable(); await filterBar.addFilter(TEST_ANCHOR_FILTER_FIELD, 'IS', TEST_ANCHOR_FILTER_VALUE); await PageObjects.context.waitUntilContextLoadingHasFinished(); // disable filter await filterBar.toggleFilterEnabled(TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false)).to.be(true); - - const rows = await docTable.getBodyRows(table); - const hasOnlyFilteredRows = ( - await Promise.all(rows.map( - async (row) => await (await docTable.getFields(row))[2].getVisibleText() - )) - ).every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); - expect(hasOnlyFilteredRows).to.be(false); + retry.try(async () => { + expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false)).to.be(true); + const fields = await docTable.getFields(); + const hasOnlyFilteredRows = fields + .map(row => row[2]) + .every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); + expect(hasOnlyFilteredRows).to.be(false); + }); }); }); } diff --git a/test/functional/apps/context/_size.js b/test/functional/apps/context/_size.js index 08f2b1e9e519..9b693d2cda89 100644 --- a/test/functional/apps/context/_size.js +++ b/test/functional/apps/context/_size.js @@ -42,9 +42,8 @@ export default function ({ getService, getPageObjects }) { it('should default to the `context:defaultSize` setting', async function () { await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_TYPE, TEST_ANCHOR_ID); - const table = await docTable.getTable(); await retry.try(async function () { - expect(await docTable.getBodyRows(table)).to.have.length(2 * TEST_DEFAULT_CONTEXT_SIZE + 1); + expect(await docTable.getRowsText()).to.have.length(2 * TEST_DEFAULT_CONTEXT_SIZE + 1); }); await retry.try(async function () { const predecessorCountPicker = await PageObjects.context.getPredecessorCountPicker(); @@ -58,12 +57,10 @@ export default function ({ getService, getPageObjects }) { it('should increase according to the `context:step` setting when clicking the `load newer` button', async function () { await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_TYPE, TEST_ANCHOR_ID); - - const table = await docTable.getTable(); await PageObjects.context.clickPredecessorLoadMoreButton(); await retry.try(async function () { - expect(await docTable.getBodyRows(table)).to.have.length( + expect(await docTable.getRowsText()).to.have.length( 2 * TEST_DEFAULT_CONTEXT_SIZE + TEST_STEP_SIZE + 1 ); }); @@ -71,12 +68,10 @@ export default function ({ getService, getPageObjects }) { it('should increase according to the `context:step` setting when clicking the `load older` button', async function () { await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_TYPE, TEST_ANCHOR_ID); - - const table = await docTable.getTable(); await PageObjects.context.clickSuccessorLoadMoreButton(); await retry.try(async function () { - expect(await docTable.getBodyRows(table)).to.have.length( + expect(await docTable.getRowsText()).to.have.length( 2 * TEST_DEFAULT_CONTEXT_SIZE + TEST_STEP_SIZE + 1 ); }); diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index 97b5cc6bae58..e9afa7c9b4c1 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -46,13 +46,9 @@ export default function ({ getService, getPageObjects }) { }); it('should open the doc view of the selected document', async function () { - const discoverDocTable = await docTable.getTable(); - const firstRow = (await docTable.getBodyRows(discoverDocTable))[0]; - // navigate to the doc view - await (await docTable.getRowExpandToggle(firstRow)).click(); - const firstDetailsRow = (await docTable.getDetailsRows(discoverDocTable))[0]; - await (await docTable.getRowActions(firstDetailsRow))[1].click(); + await docTable.clickRowToggle({ rowIndex: 0 }); + await (await docTable.getRowActions({ rowIndex: 0 }))[1].click(); const hasDocHit = await testSubjects.exists('doc-hit'); expect(hasDocHit).to.be(true); diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index 4b85593d1f02..f0d34207a87a 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -71,7 +71,7 @@ export default function ({ getService, getPageObjects }) { ':(from:\'2015-09-19T06:31:44.000Z\',to:\'2015-09' + '-23T18:31:44.000Z\'))&_a=(columns:!(_source),index:\'logstash-' + '*\',interval:auto,query:(language:kuery,query:\'\')' + - ',sort:!(\'@timestamp\',desc))'; + ',sort:!(!(\'@timestamp\',desc)))'; const actualUrl = await PageObjects.share.getSharedUrl(); // strip the timestamp out of each URL expect(actualUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')).to.be( diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index 8da4b217f2b7..26eae542145c 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -132,15 +132,93 @@ export default function ({ getService, getPageObjects }) { await inspector.open(); await inspector.setTablePageSize(50); await inspector.expectTableData(expectedTableData); + await inspector.close(); }); - it('should hide side editor if embed is set to true in url', async () => { - const url = await browser.getCurrentUrl(); - const embedUrl = url.split('/visualize/').pop().replace('?_g=', '?embed=true&_g='); - await PageObjects.common.navigateToUrl('visualize', embedUrl); - await PageObjects.header.waitUntilLoadingHasFinished(); - const sideEditorExists = await PageObjects.visualize.getSideEditorExists(); - expect(sideEditorExists).to.be(false); + describe('axis scaling', () => { + it('scales count agg', async () => { + const expectedTableData = [ + [ '2015-09-20 00:00', '0.002' ], + [ '2015-09-20 01:00', '0.003' ], + [ '2015-09-20 02:00', '0.006' ], + [ '2015-09-20 03:00', '0.009' ], + [ '2015-09-20 04:00', '0.014' ], + [ '2015-09-20 05:00', '0.033' ], + [ '2015-09-20 06:00', '0.05' ], + [ '2015-09-20 07:00', '0.061' ], + [ '2015-09-20 08:00', '0.095' ], + [ '2015-09-20 09:00', '0.122' ], + [ '2015-09-20 10:00', '0.133' ], + [ '2015-09-20 11:00', '0.144' ], + [ '2015-09-20 12:00', '0.145' ], + [ '2015-09-20 13:00', '0.124' ], + [ '2015-09-20 14:00', '0.112' ], + [ '2015-09-20 15:00', '0.089' ], + [ '2015-09-20 16:00', '0.072' ], + [ '2015-09-20 17:00', '0.048' ], + [ '2015-09-20 18:00', '0.026' ], + [ '2015-09-20 19:00', '0.015' ], + ]; + + await PageObjects.visualize.toggleOpenEditor(2); + await PageObjects.visualize.setInterval('Second'); + await PageObjects.visualize.toggleOpenEditor(2, 'false'); + await PageObjects.visualize.clickGo(); + await inspector.open(); + await inspector.expectTableData(expectedTableData); + await inspector.close(); + }); + + it('does not scale top hit agg', async () => { + const expectedTableData = [ + [ '2015-09-20 00:00', '6', '9.035KB' ], + [ '2015-09-20 01:00', '9', '5.854KB' ], + [ '2015-09-20 02:00', '22', '4.588KB' ], + [ '2015-09-20 03:00', '31', '8.349KB' ], + [ '2015-09-20 04:00', '52', '2.637KB' ], + [ '2015-09-20 05:00', '119', '1.712KB' ], + [ '2015-09-20 06:00', '181', '9.157KB' ], + [ '2015-09-20 07:00', '218', '8.192KB' ], + [ '2015-09-20 08:00', '341', '12.384KB' ], + [ '2015-09-20 09:00', '440', '4.482KB' ], + [ '2015-09-20 10:00', '480', '9.449KB' ], + [ '2015-09-20 11:00', '517', '213B' ], + [ '2015-09-20 12:00', '522', '638B' ], + [ '2015-09-20 13:00', '446', '7.421KB' ], + [ '2015-09-20 14:00', '403', '4.854KB' ], + [ '2015-09-20 15:00', '321', '4.132KB' ], + [ '2015-09-20 16:00', '258', '601B' ], + [ '2015-09-20 17:00', '172', '4.239KB' ], + [ '2015-09-20 18:00', '95', '6.272KB' ], + [ '2015-09-20 19:00', '55', '2.053KB' ] + ]; + + await PageObjects.visualize.clickBucket('Y-axis', 'metrics'); + await PageObjects.visualize.selectAggregation('Top Hit', 'metrics'); + await PageObjects.visualize.selectField('bytes', 'metrics'); + await PageObjects.visualize.selectAggregateWith('average'); + await PageObjects.visualize.clickGo(); + await inspector.open(); + await inspector.expectTableData(expectedTableData); + await inspector.close(); + }); + }); + + describe('embedded mode', () => { + it('should hide side editor if embed is set to true in url', async () => { + const url = await browser.getCurrentUrl(); + const embedUrl = url.split('/visualize/').pop().replace('?_g=', '?embed=true&_g='); + await PageObjects.common.navigateToUrl('visualize', embedUrl); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sideEditorExists = await PageObjects.visualize.getSideEditorExists(); + expect(sideEditorExists).to.be(false); + }); + + after(async () => { + const url = await browser.getCurrentUrl(); + const embedUrl = url.split('/visualize/').pop().replace('?embed=true&', '?'); + await PageObjects.common.navigateToUrl('visualize', embedUrl); + }); }); describe.skip('switch between Y axis scale types', () => { diff --git a/test/functional/apps/visualize/_gauge_chart.js b/test/functional/apps/visualize/_gauge_chart.js index 6b14cabb54f9..a1d5a49f1a60 100644 --- a/test/functional/apps/visualize/_gauge_chart.js +++ b/test/functional/apps/visualize/_gauge_chart.js @@ -57,7 +57,6 @@ export default function ({ getService, getPageObjects }) { }); it('should show Split Gauges', async function () { - await PageObjects.visualize.clickMetricEditor(); log.debug('Bucket = Split Group'); await PageObjects.visualize.clickBucket('Split group'); log.debug('Aggregation = Terms'); @@ -81,7 +80,6 @@ export default function ({ getService, getPageObjects }) { it('should show correct values for fields with fieldFormatters', async function () { const expectedTexts = [ '2,904', 'win 8: Count', '0B', 'win 8: Min bytes' ]; - await PageObjects.visualize.clickMetricEditor(); await PageObjects.visualize.selectAggregation('Terms'); await PageObjects.visualize.selectField('machine.os.raw'); await PageObjects.visualize.setSize('1'); diff --git a/test/functional/apps/visualize/_metric_chart.js b/test/functional/apps/visualize/_metric_chart.js index bb75ef6648d7..237ee1ef50b1 100644 --- a/test/functional/apps/visualize/_metric_chart.js +++ b/test/functional/apps/visualize/_metric_chart.js @@ -183,7 +183,6 @@ export default function ({ getService, getPageObjects }) { }); it('should allow filtering with buckets', async function () { - await PageObjects.visualize.clickMetricEditor(); log.debug('Bucket = Split Group'); await PageObjects.visualize.clickBucket('Split group'); log.debug('Aggregation = Terms'); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index f3f285c03cfc..0a2400a367a7 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -109,7 +109,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); // FLAKY: https://github.com/elastic/kibana/issues/40458 - it.skip('should show the correct count in the legend with "Human readable" duration formatter', async () => { + it('should show the correct count in the legend with "Human readable" duration formatter', async () => { await visualBuilder.clickSeriesOption(); await visualBuilder.changeDataFormatter('Duration'); await visualBuilder.setDurationFormatterSettings({ to: 'Human readable' }); diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index b246ea1ad08d..7798ec399981 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -434,6 +434,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { // Note: this replacement of - to space is to preserve original logic but I'm not sure why or if it's needed. await searchFilter.type(dashName.replace('-', ' ')); await PageObjects.common.pressEnterKey(); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading', 5000); }); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -460,9 +461,12 @@ export function DashboardPageProvider({ getService, getPageObjects }) { await this.gotoDashboardLandingPage(); await this.searchForDashboardWithName(dashName); - await this.selectDashboard(dashName); - await PageObjects.header.waitUntilLoadingHasFinished(); - + await retry.try(async () => { + await this.selectDashboard(dashName); + await PageObjects.header.waitUntilLoadingHasFinished(); + // check Dashboard landing page is not present + await testSubjects.missingOrFail('dashboardLandingPage', { timeout: 10000 }); + }); } async getPanelTitles() { diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 7630aef71dde..6954bed43847 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -248,7 +248,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { } async expectMissingFieldListItemVisualize(field) { - await testSubjects.missingOrFail(`fieldVisualize-${field}`); + await testSubjects.missingOrFail(`fieldVisualize-${field}`, { allowHidden: true }); } async clickFieldListPlusFilter(field, value) { @@ -288,7 +288,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { const fieldFilterFormExists = await testSubjects.exists('discoverFieldFilter'); if (fieldFilterFormExists) { await testSubjects.click('toggleFieldFilterButton'); - await testSubjects.missingOrFail('discoverFieldFilter'); + await testSubjects.missingOrFail('discoverFieldFilter', { allowHidden: true }); } } diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index c84debcfecbd..2ddaea944fd9 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -407,7 +407,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli } async clickMetricEditor() { - await find.clickByCssSelector('button[data-test-subj="toggleEditor"]'); + await find.clickByCssSelector('[group-name="metrics"] .euiAccordion__button'); } async clickMetricByIndex(index) { @@ -450,8 +450,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli async selectAggregation(myString, groupName = 'buckets', childAggregationType = null) { const comboBoxElement = await find.byCssSelector(` [group-name="${groupName}"] - vis-editor-agg-params:not(.ng-hide) - [data-test-subj="visAggEditorParams"] + [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen ${childAggregationType ? '.visEditorAgg__subAgg' : ''} [data-test-subj="defaultEditorAggSelect"] `); @@ -479,7 +478,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli async toggleOpenEditor(index, toState = 'true') { // index, see selectYAxisAggregation - const toggle = await find.byCssSelector(`button[aria-controls="visAggEditorParams${index}"]`); + const toggle = await find.byCssSelector(`button[aria-controls="visEditorAggAccordion${index}"]`); const toggleOpen = await toggle.getAttribute('aria-expanded'); log.debug(`toggle ${index} expand = ${toggleOpen}`); if (toggleOpen !== toState) { @@ -497,12 +496,10 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli // select our agg const aggSelect = await find - .byCssSelector(`[data-test-subj="aggregationEditor${index}"] - vis-editor-agg-params:not(.ng-hide) [data-test-subj="defaultEditorAggSelect"]`); + .byCssSelector(`#visEditorAggAccordion${index} [data-test-subj="defaultEditorAggSelect"]`); await comboBox.setElement(aggSelect, agg); - const fieldSelect = await find.byCssSelector(`[data-test-subj="aggregationEditor${index}"] - vis-editor-agg-params:not(.ng-hide) [data-test-subj="visDefaultEditorField"]`); + const fieldSelect = await find.byCssSelector(`#visEditorAggAccordion${index} [data-test-subj="visDefaultEditorField"]`); // select our field await comboBox.setElement(fieldSelect, field); // enter custom label @@ -553,7 +550,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli log.debug(`selectField ${fieldValue}`); const selector = ` [group-name="${groupName}"] - vis-editor-agg-params:not(.ng-hide) + [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen [data-test-subj="visAggEditorParams"] ${childAggregationType ? '.visEditorAgg__subAgg' : ''} [data-test-subj="visDefaultEditorField"] @@ -562,6 +559,12 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli await comboBox.setElement(fieldEl, fieldValue); } + async selectAggregateWith(fieldValue) { + const sortSelect = await testSubjects.find(`visDefaultEditorAggregateWith`); + const sortMetric = await sortSelect.findByCssSelector(`option[value="${fieldValue}"]`); + await sortMetric.click(); + } + async selectFieldById(fieldValue, id) { await find.clickByCssSelector(`#${id} > option[label="${fieldValue}"]`); } @@ -593,26 +596,26 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli } async setSize(newValue, aggId) { - const dataTestSubj = aggId ? `aggregationEditor${aggId} sizeParamEditor` : 'sizeParamEditor'; + const dataTestSubj = aggId ? `visEditorAggAccordion${aggId} sizeParamEditor` : 'sizeParamEditor'; await testSubjects.setValue(dataTestSubj, String(newValue)); } async toggleDisabledAgg(agg) { - await testSubjects.click(`aggregationEditor${agg} disableAggregationBtn`); + await testSubjects.click(`visEditorAggAccordion${agg} toggleDisableAggregationBtn`); await PageObjects.header.waitUntilLoadingHasFinished(); } async toggleAggregationEditor(agg) { - await testSubjects.click(`aggregationEditor${agg} toggleEditor`); + await find.clickByCssSelector(`[data-test-subj="visEditorAggAccordion${agg}"] .euiAccordion__button`); await PageObjects.header.waitUntilLoadingHasFinished(); } async toggleOtherBucket(agg = 2) { - return await testSubjects.click(`aggregationEditor${agg} otherBucketSwitch`); + return await testSubjects.click(`visEditorAggAccordion${agg} otherBucketSwitch`); } async toggleMissingBucket(agg = 2) { - return await testSubjects.click(`aggregationEditor${agg} missingBucketSwitch`); + return await testSubjects.click(`visEditorAggAccordion${agg} missingBucketSwitch`); } async isApplyEnabled() { @@ -1271,7 +1274,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli } async removeDimension(agg) { - await testSubjects.click(`aggregationEditor${agg} removeDimensionBtn`); + await testSubjects.click(`visEditorAggAccordion${agg} removeDimensionBtn`); } } diff --git a/test/functional/services/apps_menu.ts b/test/functional/services/apps_menu.ts index 93858cdb4452..a4cd98b2a06e 100644 --- a/test/functional/services/apps_menu.ts +++ b/test/functional/services/apps_menu.ts @@ -31,12 +31,9 @@ export function AppsMenuProvider({ getService }: FtrProviderContext) { const appMenu = await testSubjects.find('navDrawer'); const $ = await appMenu.parseDomContent(); - const links: Array<{ - text: string; - href: string; - }> = $.findTestSubjects('navDrawerAppsMenuLink') + const links = $.findTestSubjects('navDrawerAppsMenuLink') .toArray() - .map((link: any) => { + .map(link => { return { text: $(link).text(), href: $(link).attr('href'), diff --git a/test/functional/services/doc_table.js b/test/functional/services/doc_table.js deleted file mode 100644 index 5992ee7c5e57..000000000000 --- a/test/functional/services/doc_table.js +++ /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. - */ - -export function DocTableProvider({ getService, getPageObjects }) { - const testSubjects = getService('testSubjects'); - const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'header']); - - - class DocTable { - async getTable() { - return await testSubjects.find('docTable'); - } - - async getBodyRows(table) { - return await table.findAllByCssSelector('[data-test-subj~="docTableRow"]'); - } - - async getAnchorRow(table) { - return await table.findByCssSelector('[data-test-subj~="docTableAnchorRow"]'); - } - - async getAnchorDetailsRow(table) { - return await table.findByCssSelector('[data-test-subj~="docTableAnchorRow"] + [data-test-subj~="docTableDetailsRow"]'); - } - - async getRowExpandToggle(row) { - return await row.findByCssSelector('[data-test-subj~="docTableExpandToggleColumn"]'); - } - - async getDetailsRows(table) { - return await table.findAllByCssSelector('[data-test-subj~="docTableRow"] + [data-test-subj~="docTableDetailsRow"]'); - } - - async getRowActions(row) { - return await row.findAllByCssSelector('[data-test-subj~="docTableRowAction"]'); - } - - async getFields(row) { - return await row.findAllByCssSelector('[data-test-subj~="docTableField"]'); - } - - async getHeaderFields(table) { - return await table.findAllByCssSelector('[data-test-subj~="docTableHeaderField"]'); - } - - async getTableDocViewRow(detailsRow, fieldName) { - return await detailsRow.findByCssSelector(`[data-test-subj~="tableDocViewRow-${fieldName}"]`); - } - - async getAddInclusiveFilterButton(tableDocViewRow) { - return await tableDocViewRow.findByCssSelector(`[data-test-subj~="addInclusiveFilterButton"]`); - } - - async addInclusiveFilter(detailsRow, fieldName) { - const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); - const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); - await addInclusiveFilterButton.click(); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - } - - async toggleRowExpanded(row) { - const rowExpandToggle = await this.getRowExpandToggle(row); - await rowExpandToggle.click(); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - - const detailsRow = await row.findByXpath('./following-sibling::*[@data-test-subj="docTableDetailsRow"]'); - return await retry.try(async () => { - return detailsRow.findByCssSelector('[data-test-subj~="docViewer"]'); - }); - } - } - - return new DocTable(); -} diff --git a/test/functional/services/doc_table.ts b/test/functional/services/doc_table.ts new file mode 100644 index 000000000000..e09500317cd3 --- /dev/null +++ b/test/functional/services/doc_table.ts @@ -0,0 +1,165 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { FtrProviderContext } from '../ftr_provider_context'; +import { WebElementWrapper } from './lib/web_element_wrapper'; + +export function DocTableProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'header']); + + interface SelectOptions { + isAnchorRow: boolean; + rowIndex: number; + } + + class DocTable { + public async getTable() { + return await testSubjects.find('docTable'); + } + + public async getRowsText() { + const table = await this.getTable(); + const $ = await table.parseDomContent(); + return $.findTestSubjects('docTableRow') + .toArray() + .map((row: any) => + $(row) + .text() + .trim() + ); + } + + public async getBodyRows(): Promise { + const table = await this.getTable(); + return await table.findAllByCssSelector('[data-test-subj~="docTableRow"]'); + } + + public async getAnchorRow(): Promise { + const table = await this.getTable(); + return await table.findByCssSelector('[data-test-subj~="docTableAnchorRow"]'); + } + + public async getRow(options: SelectOptions): Promise { + return options.isAnchorRow + ? await this.getAnchorRow() + : (await this.getBodyRows())[options.rowIndex]; + } + + public async getAnchorDetailsRow(): Promise { + const table = await this.getTable(); + return await table.findByCssSelector( + '[data-test-subj~="docTableAnchorRow"] + [data-test-subj~="docTableDetailsRow"]' + ); + } + + public async clickRowToggle( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + const row = await this.getRow(options); + const toggle = await row.findByCssSelector('[data-test-subj~="docTableExpandToggleColumn"]'); + await toggle.click(); + } + + public async getDetailsRows(): Promise { + const table = await this.getTable(); + return await table.findAllByCssSelector( + '[data-test-subj~="docTableRow"] + [data-test-subj~="docTableDetailsRow"]' + ); + } + + public async getRowActions( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + const detailsRow = options.isAnchorRow + ? await this.getAnchorDetailsRow() + : (await this.getDetailsRows())[options.rowIndex]; + return await detailsRow.findAllByCssSelector('[data-test-subj~="docTableRowAction"]'); + } + + public async getFields(options: { isAnchorRow: boolean } = { isAnchorRow: false }) { + const table = await this.getTable(); + const $ = await table.parseDomContent(); + const rowLocator = options.isAnchorRow ? 'docTableAnchorRow' : 'docTableRow'; + const rows = $.findTestSubjects(rowLocator).toArray(); + const fields = rows.map((row: any) => + $(row) + .find('[data-test-subj~="docTableField"]') + .toArray() + .map((field: any) => $(field).text()) + ); + return fields; + } + + public async getHeaderFields(): Promise { + const table = await this.getTable(); + const $ = await table.parseDomContent(); + return $.findTestSubjects('docTableHeaderField') + .toArray() + .map((field: any) => + $(field) + .text() + .trim() + ); + } + + public async getTableDocViewRow( + detailsRow: WebElementWrapper, + fieldName: WebElementWrapper + ): Promise { + return await detailsRow.findByCssSelector(`[data-test-subj~="tableDocViewRow-${fieldName}"]`); + } + + public async getAddInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByCssSelector( + `[data-test-subj~="addInclusiveFilterButton"]` + ); + } + + public async addInclusiveFilter( + detailsRow: WebElementWrapper, + fieldName: WebElementWrapper + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + + public async toggleRowExpanded( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + await this.clickRowToggle(options); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + return await retry.try(async () => { + const row = options.isAnchorRow + ? await this.getAnchorRow() + : (await this.getBodyRows())[options.rowIndex]; + const detailsRow = await row.findByXpath( + './following-sibling::*[@data-test-subj="docTableDetailsRow"]' + ); + return detailsRow.findByCssSelector('[data-test-subj~="docViewer"]'); + }); + } + } + + return new DocTable(); +} diff --git a/test/functional/services/find.ts b/test/functional/services/find.ts index e94e1975231c..4fdc26619d8b 100644 --- a/test/functional/services/find.ts +++ b/test/functional/services/find.ts @@ -32,6 +32,7 @@ export async function FindProvider({ getService }: FtrProviderContext) { const browserType = webdriver.browserType; const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists'); + const POLLING_TIME = 500; const defaultFindTimeout = config.get('timeouts.find'); const fixedHeaderHeight = config.get('layout.fixedHeaderHeight'); @@ -426,7 +427,7 @@ export async function FindProvider({ getService }: FtrProviderContext) { timeout: number = defaultFindTimeout ) { log.debug(`Find.waitForDeletedByCssSelector('${selector}') with timeout=${timeout}`); - await this._withTimeout(1000); + await this._withTimeout(POLLING_TIME); await driver.wait( async () => { const found = await driver.findElements(By.css(selector)); diff --git a/test/functional/services/inspector.js b/test/functional/services/inspector.js index 67d3c1113103..9c25ebea48b4 100644 --- a/test/functional/services/inspector.js +++ b/test/functional/services/inspector.js @@ -85,7 +85,7 @@ export function InspectorProvider({ getService }) { // The buttons for setting table page size are in a popover element. This popover // element appears as if it's part of the inspectorPanel but it's really attached // to the body element by a portal. - const tableSizesPopover = await find.byCssSelector('.euiPanel'); + const tableSizesPopover = await find.byCssSelector('.euiPanel .euiContextMenuPanel'); await find.clickByButtonText(`${size} rows`, tableSizesPopover); } diff --git a/test/functional/services/lib/web_element_wrapper/custom_cheerio_api.ts b/test/functional/services/lib/web_element_wrapper/custom_cheerio_api.ts new file mode 100644 index 000000000000..301eb656ed6f --- /dev/null +++ b/test/functional/services/lib/web_element_wrapper/custom_cheerio_api.ts @@ -0,0 +1,246 @@ +/* eslint-disable */ + +/** + * Type interfaces extracted from node_modules/@types/cheerio/index.d.ts + * and customized to include our custom methods + */ + +interface CheerioSelector { + (selector: string): CustomCheerio; + (selector: string, context: string): CustomCheerio; + (selector: string, context: CheerioElement): CustomCheerio; + (selector: string, context: CheerioElement[]): CustomCheerio; + (selector: string, context: Cheerio): CustomCheerio; + (selector: string, context: string, root: string): CustomCheerio; + (selector: string, context: CheerioElement, root: string): CustomCheerio; + (selector: string, context: CheerioElement[], root: string): CustomCheerio; + (selector: string, context: Cheerio, root: string): CustomCheerio; + (selector: any): CustomCheerio; +} + +export interface CustomCheerioStatic extends CheerioSelector { + // Document References + // Cheerio https://github.com/cheeriojs/cheerio + // JQuery http://api.jquery.com + xml(): string; + root(): CustomCheerio; + contains(container: CheerioElement, contained: CheerioElement): boolean; + parseHTML(data: string, context?: Document, keepScripts?: boolean): Document[]; + + html(options?: CheerioOptionsInterface): string; + html(selector: string, options?: CheerioOptionsInterface): string; + html(element: CustomCheerio, options?: CheerioOptionsInterface): string; + html(element: CheerioElement, options?: CheerioOptionsInterface): string; + + // + // CUSTOM METHODS + // + findTestSubjects(selector: string): CustomCheerio; + findTestSubject(selector: string): CustomCheerio; +} + +export interface CustomCheerio { + // Document References + // Cheerio https://github.com/cheeriojs/cheerio + // JQuery http://api.jquery.com + + [index: number]: CheerioElement; + length: number; + + // Attributes + + attr(): { [attr: string]: string }; + attr(name: string): string; + attr(name: string, value: any): CustomCheerio; + + data(): any; + data(name: string): any; + data(name: string, value: any): any; + + val(): string; + val(value: string): CustomCheerio; + + removeAttr(name: string): CustomCheerio; + + has(selector: string): CustomCheerio; + has(element: CheerioElement): CustomCheerio; + + hasClass(className: string): boolean; + addClass(classNames: string): CustomCheerio; + + removeClass(): CustomCheerio; + removeClass(className: string): CustomCheerio; + removeClass(func: (index: number, className: string) => string): CustomCheerio; + + toggleClass(className: string): CustomCheerio; + toggleClass(className: string, toggleSwitch: boolean): CustomCheerio; + toggleClass(toggleSwitch?: boolean): CustomCheerio; + toggleClass( + func: (index: number, className: string, toggleSwitch: boolean) => string, + toggleSwitch?: boolean + ): CustomCheerio; + + is(selector: string): boolean; + is(element: CheerioElement): boolean; + is(element: CheerioElement[]): boolean; + is(selection: CustomCheerio): boolean; + is(func: (index: number, element: CheerioElement) => boolean): boolean; + + // Form + serialize(): string; + serializeArray(): Array<{ name: string; value: string }>; + + // Traversing + + find(selector: string): CustomCheerio; + find(element: CustomCheerio): CustomCheerio; + + parent(selector?: string): CustomCheerio; + parents(selector?: string): CustomCheerio; + parentsUntil(selector?: string, filter?: string): CustomCheerio; + parentsUntil(element: CheerioElement, filter?: string): CustomCheerio; + parentsUntil(element: CustomCheerio, filter?: string): CustomCheerio; + + prop(name: string): any; + prop(name: string, value: any): CustomCheerio; + + closest(): CustomCheerio; + closest(selector: string): CustomCheerio; + + next(selector?: string): CustomCheerio; + nextAll(): CustomCheerio; + nextAll(selector: string): CustomCheerio; + + nextUntil(selector?: string, filter?: string): CustomCheerio; + nextUntil(element: CheerioElement, filter?: string): CustomCheerio; + nextUntil(element: CustomCheerio, filter?: string): CustomCheerio; + + prev(selector?: string): CustomCheerio; + prevAll(): CustomCheerio; + prevAll(selector: string): CustomCheerio; + + prevUntil(selector?: string, filter?: string): CustomCheerio; + prevUntil(element: CheerioElement, filter?: string): CustomCheerio; + prevUntil(element: CustomCheerio, filter?: string): CustomCheerio; + + slice(start: number, end?: number): CustomCheerio; + + siblings(selector?: string): CustomCheerio; + + children(selector?: string): CustomCheerio; + + contents(): CustomCheerio; + + each(func: (index: number, element: CheerioElement) => any): CustomCheerio; + map(func: (index: number, element: CheerioElement) => any): CustomCheerio; + + filter(selector: string): CustomCheerio; + filter(selection: CustomCheerio): CustomCheerio; + filter(element: CheerioElement): CustomCheerio; + filter(elements: CheerioElement[]): CustomCheerio; + filter(func: (index: number, element: CheerioElement) => boolean): CustomCheerio; + + not(selector: string): CustomCheerio; + not(selection: CustomCheerio): CustomCheerio; + not(element: CheerioElement): CustomCheerio; + not(func: (index: number, element: CheerioElement) => boolean): CustomCheerio; + + first(): CustomCheerio; + last(): CustomCheerio; + + eq(index: number): CustomCheerio; + + get(): any[]; + get(index: number): any; + + index(): number; + index(selector: string): number; + index(selection: CustomCheerio): number; + + end(): CustomCheerio; + + add(selectorOrHtml: string): CustomCheerio; + add(selector: string, context: Document): CustomCheerio; + add(element: CheerioElement): CustomCheerio; + add(elements: CheerioElement[]): CustomCheerio; + add(selection: CustomCheerio): CustomCheerio; + + addBack(): CustomCheerio; + addBack(filter: string): CustomCheerio; + + // Manipulation + appendTo(target: CustomCheerio): CustomCheerio; + prependTo(target: CustomCheerio): CustomCheerio; + + append(content: string, ...contents: any[]): CustomCheerio; + append(content: Document, ...contents: any[]): CustomCheerio; + append(content: Document[], ...contents: any[]): CustomCheerio; + append(content: CustomCheerio, ...contents: any[]): CustomCheerio; + + prepend(content: string, ...contents: any[]): CustomCheerio; + prepend(content: Document, ...contents: any[]): CustomCheerio; + prepend(content: Document[], ...contents: any[]): CustomCheerio; + prepend(content: CustomCheerio, ...contents: any[]): CustomCheerio; + + after(content: string, ...contents: any[]): CustomCheerio; + after(content: Document, ...contents: any[]): CustomCheerio; + after(content: Document[], ...contents: any[]): CustomCheerio; + after(content: CustomCheerio, ...contents: any[]): CustomCheerio; + + insertAfter(content: string): CustomCheerio; + insertAfter(content: Document): CustomCheerio; + insertAfter(content: CustomCheerio): CustomCheerio; + + before(content: string, ...contents: any[]): CustomCheerio; + before(content: Document, ...contents: any[]): CustomCheerio; + before(content: Document[], ...contents: any[]): CustomCheerio; + before(content: CustomCheerio, ...contents: any[]): CustomCheerio; + + insertBefore(content: string): CustomCheerio; + insertBefore(content: Document): CustomCheerio; + insertBefore(content: CustomCheerio): CustomCheerio; + + remove(selector?: string): CustomCheerio; + + replaceWith(content: string): CustomCheerio; + replaceWith(content: CheerioElement): CustomCheerio; + replaceWith(content: CheerioElement[]): CustomCheerio; + replaceWith(content: CustomCheerio): CustomCheerio; + replaceWith(content: () => CustomCheerio): CustomCheerio; + + empty(): CustomCheerio; + + html(): string | null; + html(html: string): CustomCheerio; + + text(): string; + text(text: string): CustomCheerio; + + wrap(content: string): CustomCheerio; + wrap(content: Document): CustomCheerio; + wrap(content: CustomCheerio): CustomCheerio; + + css(propertyName: string): string; + css(propertyNames: string[]): string[]; + css(propertyName: string, value: string): CustomCheerio; + css(propertyName: string, value: number): CustomCheerio; + css(propertyName: string, func: (index: number, value: string) => string): CustomCheerio; + css(propertyName: string, func: (index: number, value: string) => number): CustomCheerio; + css(properties: Record): CustomCheerio; + + // Rendering + + // Miscellaneous + + clone(): CustomCheerio; + + // Not Documented + + toArray(): CheerioElement[]; + + // + // CUSTOM METHODS + // + findTestSubjects(selector: string): CustomCheerio; + findTestSubject(selector: string): CustomCheerio; +} diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 2ae461628320..b05485618da0 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -25,6 +25,7 @@ import { PNG } from 'pngjs'; import cheerio from 'cheerio'; import testSubjSelector from '@kbn/test-subj-selector'; import { ToolingLog } from '@kbn/dev-utils'; +import { CustomCheerio, CustomCheerioStatic } from './custom_cheerio_api'; // @ts-ignore not supported yet import { scrollIntoViewIfNecessary } from './scroll_into_view_if_necessary'; import { Browsers } from '../../remote/browsers'; @@ -650,24 +651,28 @@ export class WebElementWrapper { * Gets element innerHTML and wrap it up with cheerio * * @nonstandard - * @return {Promise} + * @return {Promise} */ - public async parseDomContent(): Promise { + public async parseDomContent(): Promise { const htmlContent: any = await this.getAttribute('innerHTML'); const $: any = cheerio.load(htmlContent, { normalizeWhitespace: true, xmlMode: true, }); - $.findTestSubjects = function testSubjects(selector: string) { + $.findTestSubjects = function findTestSubjects(this: CustomCheerioStatic, selector: string) { return this(testSubjSelector(selector)); }; - $.fn.findTestSubjects = function testSubjects(selector: string) { + $.fn.findTestSubjects = function findTestSubjects(this: CustomCheerio, selector: string) { return this.find(testSubjSelector(selector)); }; - $.findTestSubject = $.fn.findTestSubject = function testSubjects(selector: string) { + $.findTestSubject = function findTestSubject(this: CustomCheerioStatic, selector: string) { + return this.findTestSubjects(selector).first(); + }; + + $.fn.findTestSubject = function findTestSubject(this: CustomCheerio, selector: string) { return this.findTestSubjects(selector).first(); }; diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 931544d60225..6377d97dad28 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -38,7 +38,8 @@ import { preventParallelCalls } from './prevent_parallel_calls'; import { Browsers } from './browsers'; -const throttleOption = process.env.TEST_THROTTLE_NETWORK; +const throttleOption: string = process.env.TEST_THROTTLE_NETWORK as string; +const headlessBrowser: string = process.env.TEST_BROWSER_HEADLESS as string; const SECOND = 1000; const MINUTE = 60 * SECOND; const NO_QUEUE_COMMANDS = ['getLog', 'getStatus', 'newSession', 'quit']; @@ -73,7 +74,7 @@ async function attemptToCreateCommand(log: ToolingLog, browserType: Browsers) { 'use-fake-device-for-media-stream', 'use-fake-ui-for-media-stream', ]; - if (process.env.TEST_BROWSER_HEADLESS) { + if (headlessBrowser === '1') { // Use --disable-gpu to avoid an error from a missing Mesa library, as per // See: https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md chromeOptions.push('headless', 'disable-gpu'); @@ -90,7 +91,7 @@ async function attemptToCreateCommand(log: ToolingLog, browserType: Browsers) { .build(); case 'firefox': const firefoxOptions = new firefox.Options(); - if (process.env.TEST_BROWSER_HEADLESS) { + if (headlessBrowser === '1') { // See: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode firefoxOptions.addArguments('-headless'); } @@ -106,7 +107,7 @@ async function attemptToCreateCommand(log: ToolingLog, browserType: Browsers) { const session = await buildDriverInstance(); - if (throttleOption === 'true' && browserType === 'chrome') { + if (throttleOption === '1' && browserType === 'chrome') { // Only chrome supports this option. log.debug('NETWORK THROTTLED: 768k down, 256k up, 100ms latency.'); diff --git a/test/functional/services/test_subjects.ts b/test/functional/services/test_subjects.ts index ce1ba37f267e..538870ca8268 100644 --- a/test/functional/services/test_subjects.ts +++ b/test/functional/services/test_subjects.ts @@ -59,11 +59,14 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { public async missingOrFail( selector: string, - existsOptions?: ExistsOptions + options: ExistsOptions = {} ): Promise { - if (await this.exists(selector, existsOptions)) { - throw new Error(`expected testSubject(${selector}) to not exist`); - } + const { timeout = WAIT_FOR_EXISTS_TIME, allowHidden = false } = options; + + log.debug(`TestSubjects.missingOrFail(${selector})`); + return await (allowHidden + ? this.waitForHidden(selector, timeout) + : find.waitForDeletedByCssSelector(testSubjSelector(selector), timeout)); } public async append(selector: string, text: string): Promise { @@ -245,6 +248,12 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { await find.waitForAttributeToChange(testSubjSelector(selector), attribute, value); } + public async waitForHidden(selector: string, timeout?: number): Promise { + log.debug(`TestSubjects.waitForHidden(${selector})`); + const element = await this.find(selector); + await find.waitForElementHidden(element, timeout); + } + public getCssSelector(selector: string): string { return testSubjSelector(selector); } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index b2d29f553acb..f120e4051d70 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "13.0.0", + "@elastic/eui": "13.1.1", "react": "^16.8.0", "react-dom": "^16.8.0" } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js index ea5d894b21d4..bd58184cd118 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js @@ -23,8 +23,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { uiModules } from 'ui/modules'; import chrome from 'ui/chrome'; -import { RequestAdapter } from 'ui/inspector/adapters/request'; -import { DataAdapter } from 'ui/inspector/adapters/data'; +import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; import { runPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { visualizationLoader } from 'ui/visualize/loader/visualization_loader'; diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.js b/test/interpreter_functional/test_suites/run_pipeline/basic.js index 8ebb65f9a0a1..1cb064c2f5e5 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.js +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.js @@ -45,7 +45,9 @@ export default function ({ getService, updateBaselines }) { }); // rather we want to use this to do integration tests. - describe('full expression', () => { + // Failing on chromedriver 76 + // https://github.com/elastic/kibana/issues/42842 + describe.skip('full expression', () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, {"id":"2","enabled":true,"type":"terms","schema":"segment","params": @@ -75,14 +77,14 @@ export default function ({ getService, updateBaselines }) { }); // it is also possible to combine different checks - it('runs the expression and combines different checks', async () => { + it ('runs the expression and combines different checks', async () => { await (await expectExpression('combined_test', expression).steps.toMatchSnapshot()).toMatchScreenshot(); }); }); // if we want to do multiple different tests using the same data, or reusing a part of expression its // possible to retrieve the intermediate result and reuse it in later expressions - describe('reusing partial results', () => { + describe.skip('reusing partial results', () => { it ('does some screenshot comparisons', async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.js b/test/interpreter_functional/test_suites/run_pipeline/metric.js index 4dc652dea748..b772617b53bf 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/metric.js +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.js @@ -51,17 +51,23 @@ export default function ({ getService, updateBaselines }) { await (await expectExpression('metric_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); }); - it('with single metric data', async () => { + // Test fails on chromedriver 76 + // https://github.com/elastic/kibana/issues/42842 + it.skip('with single metric data', async () => { const expression = 'metricVis metric={visdimension 0}'; await (await expectExpression('metric_single_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); }); - it('with multiple metric data', async () => { + // Test fails on chromedriver 76 + // https://github.com/elastic/kibana/issues/42842 + it.skip('with multiple metric data', async () => { const expression = 'metricVis metric={visdimension 0} metric={visdimension 1}'; await expectExpression('metric_multi_metric_data', expression, dataContext).toMatchSnapshot(); }); - it('with metric and bucket data', async () => { + // Test fails on chromedriver 76 + // https://github.com/elastic/kibana/issues/42842 + it.skip('with metric and bucket data', async () => { const expression = 'metricVis metric={visdimension 0} bucket={visdimension 2}'; await (await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); }); diff --git a/test/types/mocha_decorations.d.ts b/test/mocha_decorations.d.ts similarity index 100% rename from test/types/mocha_decorations.d.ts rename to test/mocha_decorations.d.ts diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index db8e1611fc3b..c624057bdd8e 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "13.0.0", + "@elastic/eui": "13.1.1", "react": "^16.8.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js index d9f70096aa6e..7b14ecf692ff 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js @@ -27,16 +27,14 @@ import { export class SelfChangingEditor extends React.Component { onCounterChange = (ev) => { - this.props.stageEditorParams({ - counter: parseInt(ev.target.value), - }); + this.props.setValue('counter', parseInt(ev.target.value)); } render() { return ( Running jest contracts tests" +cd "$XPACK_DIR" +SLAPSHOT_ONLINE=true CONTRACT_ONLINE=true node scripts/jest_contract.js --ci --verbose +echo "" +echo "" # echo " -> Running jest integration tests" # cd "$XPACK_DIR" # node scripts/jest_integration --ci --verbose diff --git a/test/tsconfig.json b/test/tsconfig.json index 34782c7b7598..26d69347df5a 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -13,6 +13,7 @@ "include": [ "**/*.ts", "**/*.tsx", + "../typings/lodash.topath/*.ts", ], "exclude": [ "plugin_functional/plugins/**/*" diff --git a/test/types/index.ts b/test/types/index.ts deleted file mode 100644 index edfaeb574352..000000000000 --- a/test/types/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 * from './mocha_decorations'; diff --git a/typings/index.d.ts b/typings/index.d.ts index c9eb02dbedcf..1c58a92a046d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -30,3 +30,10 @@ type MethodKeysOf = { type PublicMethodsOf = Pick>; type MockedKeys = { [P in keyof T]: jest.Mocked }; + +type DeeplyMockedKeys = { + [P in keyof T]: T[P] extends (...args: any[]) => any + ? jest.MockInstance, Parameters> + : DeeplyMockedKeys; +} & + T; diff --git a/webpackShims/moment.js b/webpackShims/moment.js index c6aca40432a8..31476d18c956 100644 --- a/webpackShims/moment.js +++ b/webpackShims/moment.js @@ -17,4 +17,4 @@ * under the License. */ -module.exports = require('../node_modules/moment/min/moment.min.js'); +module.exports = require('../node_modules/moment/min/moment-with-locales.min.js'); diff --git a/x-pack/README.md b/x-pack/README.md index df6288512c12..97c538dda6c8 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -61,6 +61,8 @@ yarn test:server #### Running functional tests +For more info, see [the Elastic functional test development guide](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html). + The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.js)), and *SAML API integration tests* ([specified by this config](test/saml_api_integration/config.js)). The script runs all sets of tests sequentially like so: @@ -141,6 +143,6 @@ node ../scripts/functional_test_runner For both of the above commands, it's crucial that you pass in `--config` to specify the same config file to both commands. This makes sure that the right tests will run against the right servers. Typically a set of tests and server configuration go together. -Read more about how the scripts work [here](scripts/README.md). +Read more about how the scripts work [here](../scripts/README.md). -For a deeper dive, read more about the way functional tests and servers work [here](packages/kbn-test/README.md). +For a deeper dive, read more about the way functional tests and servers work [here](../packages/kbn-test/README.md). diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index fa8cae2b6b86..e2cb873f54fd 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -4,61 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ -export function createJestConfig({ - kibanaDirectory, - xPackKibanaDirectory, -}) { +export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { + const fileMockPath = `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`; return { rootDir: xPackKibanaDirectory, roots: [ '/plugins', '/legacy/plugins', '/legacy/server', + '/test_utils/jest/contract_tests', ], - moduleFileExtensions: [ - 'js', - 'json', - 'ts', - 'tsx', - ], + moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], moduleNameMapper: { '^ui/(.*)': `${kibanaDirectory}/src/legacy/ui/public/$1`, - 'uiExports/(.*)': `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`, + 'uiExports/(.*)': fileMockPath, '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^plugins/watcher/models/(.*)': `${xPackKibanaDirectory}/legacy/plugins/watcher/public/models/$1`, - '^plugins/([^\/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`, + '^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`, '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': - `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`, + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, - '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx` + '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`, }, setupFiles: [ `${kibanaDirectory}/src/dev/jest/setup/babel_polyfill.js`, `/dev-tools/jest/setup/polyfills.js`, `/dev-tools/jest/setup/enzyme.js`, ], - setupFilesAfterEnv: [ - `${kibanaDirectory}/src/dev/jest/setup/mocks.js`, - ], - testMatch: [ - '**/*.test.{js,ts,tsx}' - ], + setupFilesAfterEnv: [`${kibanaDirectory}/src/dev/jest/setup/mocks.js`], + testMatch: ['**/*.test.{js,ts,tsx}'], transform: { '^.+\\.(js|tsx?)$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, + '^.+\\.html?$': 'jest-raw-loader', }, transformIgnorePatterns: [ // ignore all node_modules except @elastic/eui which requires babel transforms to handle dynamic import() - '[/\\\\]node_modules(?![\\/\\\\]@elastic[\\/\\\\]eui)[/\\\\].+\\.js$' - ], - snapshotSerializers: [ - `${kibanaDirectory}/node_modules/enzyme-to-json/serializer` + '[/\\\\]node_modules(?![\\/\\\\]@elastic[\\/\\\\]eui)[/\\\\].+\\.js$', ], - 'reporters': [ + snapshotSerializers: [`${kibanaDirectory}/node_modules/enzyme-to-json/serializer`], + reporters: [ 'default', - [`${kibanaDirectory}/src/dev/jest/junit_reporter.js`, { - reportName: 'X-Pack Jest Tests', - }] + [ + `${kibanaDirectory}/src/dev/jest/junit_reporter.js`, + { + reportName: 'X-Pack Jest Tests', + }, + ], ], }; } diff --git a/x-pack/dev-tools/jest/setup/polyfills.js b/x-pack/dev-tools/jest/setup/polyfills.js index ee7de375c112..e949b0ef6fe4 100644 --- a/x-pack/dev-tools/jest/setup/polyfills.js +++ b/x-pack/dev-tools/jest/setup/polyfills.js @@ -12,15 +12,4 @@ const bluebird = require('bluebird'); bluebird.Promise.setScheduler(function (fn) { global.setImmediate.call(global, fn); }); const MutationObserver = require('mutation-observer'); -// There's a bug in mutation-observer around the `attributes` option -// https://dom.spec.whatwg.org/#mutationobserver -// If either options's attributeOldValue or attributeFilter is present and options's attributes is omitted, then set options's attributes to true. -const _observe = MutationObserver.prototype.observe; -MutationObserver.prototype.observe = function observe(target, options) { - const needsAttributes = options.hasOwnProperty('attributeOldValue') || options.hasOwnProperty('attributeFilter'); - if (needsAttributes && !options.hasOwnProperty('attributes')) { - options.attributes = true; - } - Function.prototype.call(_observe, this, target, options); -}; Object.defineProperty(window, 'MutationObserver', { value: MutationObserver }); diff --git a/x-pack/legacy/plugins/actions/README.md b/x-pack/legacy/plugins/actions/README.md index 0dca7d2e12f0..f17456d6ea39 100644 --- a/x-pack/legacy/plugins/actions/README.md +++ b/x-pack/legacy/plugins/actions/README.md @@ -72,11 +72,10 @@ Payload: |Property|Description|Type| |---|---|---| -|attributes.description|A description to reference and search in the future. This value will be used to populate dropdowns.|string| -|attributes.actionTypeId|The id value of the action type you want to call when the action executes.|string| -|attributes.actionTypeConfig|The configuration the action type expects. See related action type to see what attributes is expected. This will also validate against the action type if config validation is defined.|object| -|references|An array of `name`, `type` and `id`. This is the same as `references` in the saved objects API. See the saved objects API documentation.

    In most cases, you can leave this empty.|Array| -|migrationVersion|The version of the most recent migrations. This is the same as `migrationVersion` in the saved objects API. See the saved objects API documentation.

    In most cases, you can leave this empty.|object| +|description|A description to reference and search in the future. This value will be used to populate dropdowns.|string| +|actionTypeId|The id value of the action type you want to call when the action executes.|string| +|config|The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined.|object| +|secrets|The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined.|object| #### `DELETE /api/action/{id}`: Delete action @@ -116,10 +115,9 @@ Payload: |Property|Description|Type| |---|---|---| -|attributes.description|A description to reference and search in the future. This value will be used to populate dropdowns.|string| -|attributes.actionTypeConfig|The configuration the action type expects. See related action type to see what attributes is expected. This will also validate against the action type if config validation is defined.|object| -|references|An array of `name`, `type` and `id`. This is the same as `references` in the saved objects API. See the saved objects API documentation.

    In most cases, you can leave this empty.|Array| -|version|The document version when read|string| +|description|A description to reference and search in the future. This value will be used to populate dropdowns.|string| +|config|The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined.|object| +|secrets|The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined.|object| #### `POST /api/action/{id}/_fire`: Fire action @@ -147,8 +145,7 @@ The following table describes the properties of the `options` object. |---|---|---| |id|The id of the action you want to fire.|string| |params|The `params` value to give the action type executor.|object| -|namespace|The saved object namespace the action exists within.|string| -|basePath|This is a temporary parameter, but we need to capture and track the value of `request.getBasePath()` until future changes are made.

    In most cases this can be `undefined` unless you need cross spaces support.|string| +|spaceId|The space id the action is within.|string| ### Example @@ -157,14 +154,13 @@ This example makes action `3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5` fire an email. ``` server.plugins.actions.fire({ id: '3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5', + spaceId: 'default', // The spaceId of the action params: { from: 'example@elastic.co', to: ['destination@elastic.co'], subject: 'My email subject', body: 'My email body', }, - namespace: undefined, // The namespace the action exists within - basePath: undefined, // Usually `request.getBasePath();` or `undefined` }); ``` @@ -175,6 +171,7 @@ Kibana ships with a set of built-in action types: - server log: logs messages to the Kibana log using `server.log()` - email: send an email - slack: post a message to a slack channel +- index: index document(s) into elasticsearch ## server log, action id: `.log` @@ -247,6 +244,27 @@ This action type interfaces with the [Slack Incoming Webhooks feature](https://a |---|---|---| |message|the message text|string| + +## index, action id: `.index` + +The config and params properties are modelled after the [Watcher Index Action](https://www.elastic.co/guide/en/elastic-stack-overview/master/actions-index.html). The index can be set in the config or params, and if set in config, then the index set in the params will be ignored. + +#### config properties + +|Property|Description|Type| +|---|---|---| +|index|The Elasticsearch index to index into.|string _(optional)_| + +#### params properties + +|Property|Description|Type| +|---|---|---| +|index|The Elasticsearch index to index into.|string _(optional)_| +|doc_id|The optional _id of the document.|string _(optional)_| +|execution_time_field|The field that will store/index the action execution time.|string _(optional)_| +|refresh|Setting of the refresh policy for the write request|boolean _(optional)_| +|body|The documument body/bodies to index.|object or object[]| + # Command Line Utility The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: @@ -259,7 +277,7 @@ $ kbn-action create .slack "post to slack" '{"webhookUrl": "https://hooks.slack. "attributes": { "actionTypeId": ".slack", "description": "post to slack", - "actionTypeConfig": {} + "config": {} }, "references": [], "updated_at": "2019-06-26T17:55:42.728Z", diff --git a/x-pack/legacy/plugins/actions/mappings.json b/x-pack/legacy/plugins/actions/mappings.json index e76612ca56c4..e2649568f25e 100644 --- a/x-pack/legacy/plugins/actions/mappings.json +++ b/x-pack/legacy/plugins/actions/mappings.json @@ -7,11 +7,11 @@ "actionTypeId": { "type": "keyword" }, - "actionTypeConfig": { + "config": { "enabled": false, "type": "object" }, - "actionTypeConfigSecrets": { + "secrets": { "type": "binary" } } diff --git a/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts b/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts index a157e85d2ac2..dcb8b8b299d1 100644 --- a/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/legacy/plugins/actions/server/action_type_registry.test.ts @@ -13,6 +13,7 @@ import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/ import { ActionTypeRegistry } from './action_type_registry'; import { ExecutorType } from './types'; import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { ExecutorError } from './lib'; const mockTaskManager = taskManagerMock.create(); @@ -27,6 +28,8 @@ const actionTypeRegistryParams = { getServices, taskManager: mockTaskManager, encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.create(), + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + getBasePath: jest.fn().mockReturnValue(undefined), }; beforeEach(() => jest.resetAllMocks()); @@ -44,22 +47,23 @@ describe('register()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], executor, }); expect(actionTypeRegistry.has('my-action-type')).toEqual(true); expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1); expect(mockTaskManager.registerTaskDefinitions.mock.calls[0]).toMatchInlineSnapshot(` -Array [ - Object { - "actions:my-action-type": Object { - "createTaskRunner": [MockFunction], - "title": "My action type", - "type": "actions:my-action-type", - }, - }, -] -`); + Array [ + Object { + "actions:my-action-type": Object { + "createTaskRunner": [MockFunction], + "getRetry": [Function], + "maxAttempts": 1, + "title": "My action type", + "type": "actions:my-action-type", + }, + }, + ] + `); expect(getCreateTaskRunnerFunction).toHaveBeenCalledTimes(1); const call = getCreateTaskRunnerFunction.mock.calls[0][0]; expect(call.actionTypeRegistry).toBeTruthy(); @@ -72,20 +76,38 @@ Array [ actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], executor, }); expect(() => actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], executor, }) ).toThrowErrorMatchingInlineSnapshot( `"Action type \\"my-action-type\\" is already registered."` ); }); + + test('provides a getRetry function that handles ExecutorError', () => { + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + executor, + }); + expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1); + const registerTaskDefinitionsCall = mockTaskManager.registerTaskDefinitions.mock.calls[0][0]; + const getRetry = registerTaskDefinitionsCall['actions:my-action-type'].getRetry!; + + const retryTime = new Date(); + expect(getRetry(0, new Error())).toEqual(false); + expect(getRetry(0, new ExecutorError('my message', {}, true))).toEqual(true); + expect(getRetry(0, new ExecutorError('my message', {}, false))).toEqual(false); + expect(getRetry(0, new ExecutorError('my message', {}, null))).toEqual(false); + expect(getRetry(0, new ExecutorError('my message', {}, undefined))).toEqual(false); + expect(getRetry(0, new ExecutorError('my message', {}, retryTime))).toEqual(retryTime); + }); }); describe('get()', () => { @@ -94,18 +116,16 @@ describe('get()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], executor, }); const actionType = actionTypeRegistry.get('my-action-type'); expect(actionType).toMatchInlineSnapshot(` -Object { - "executor": [Function], - "id": "my-action-type", - "name": "My action type", - "unencryptedAttributes": Array [], -} -`); + Object { + "executor": [Function], + "id": "my-action-type", + "name": "My action type", + } + `); }); test(`throws an error when action type doesn't exist`, () => { @@ -122,7 +142,6 @@ describe('list()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], executor, }); const actionTypes = actionTypeRegistry.list(); @@ -146,7 +165,6 @@ describe('has()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], executor, }); expect(actionTypeRegistry.has('my-action-type')); diff --git a/x-pack/legacy/plugins/actions/server/action_type_registry.ts b/x-pack/legacy/plugins/actions/server/action_type_registry.ts index feba76b06b81..635d61887826 100644 --- a/x-pack/legacy/plugins/actions/server/action_type_registry.ts +++ b/x-pack/legacy/plugins/actions/server/action_type_registry.ts @@ -8,30 +8,37 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { ActionType, GetServicesFunction } from './types'; import { TaskManager, TaskRunCreatorFunction } from '../../task_manager'; -import { getCreateTaskRunnerFunction } from './lib'; +import { getCreateTaskRunnerFunction, ExecutorError } from './lib'; import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; +import { SpacesPlugin } from '../../spaces'; interface ConstructorOptions { taskManager: TaskManager; getServices: GetServicesFunction; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; + spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; + getBasePath: SpacesPlugin['getBasePath']; } export class ActionTypeRegistry { private readonly taskRunCreatorFunction: TaskRunCreatorFunction; - private readonly getServices: GetServicesFunction; private readonly taskManager: TaskManager; private readonly actionTypes: Map = new Map(); - private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; - constructor({ getServices, taskManager, encryptedSavedObjectsPlugin }: ConstructorOptions) { - this.getServices = getServices; + constructor({ + getServices, + taskManager, + encryptedSavedObjectsPlugin, + spaceIdToNamespace, + getBasePath, + }: ConstructorOptions) { this.taskManager = taskManager; - this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin; this.taskRunCreatorFunction = getCreateTaskRunnerFunction({ + getServices, actionTypeRegistry: this, - getServices: this.getServices, - encryptedSavedObjectsPlugin: this.encryptedSavedObjectsPlugin, + encryptedSavedObjectsPlugin, + spaceIdToNamespace, + getBasePath, }); } @@ -61,6 +68,14 @@ export class ActionTypeRegistry { [`actions:${actionType.id}`]: { title: actionType.name, type: `actions:${actionType.id}`, + maxAttempts: actionType.maxAttempts || 1, + getRetry(attempts: number, error: any) { + if (error instanceof ExecutorError) { + return error.retry == null ? false : error.retry; + } + // Don't retry other kinds of errors + return false; + }, createTaskRunner: this.taskRunCreatorFunction, }, }); diff --git a/x-pack/legacy/plugins/actions/server/actions_client.test.ts b/x-pack/legacy/plugins/actions/server/actions_client.test.ts index bf16b1190e7f..d724c28d72ea 100644 --- a/x-pack/legacy/plugins/actions/server/actions_client.test.ts +++ b/x-pack/legacy/plugins/actions/server/actions_client.test.ts @@ -10,16 +10,14 @@ import { ActionTypeRegistry } from './action_type_registry'; import { ActionsClient } from './actions_client'; import { ExecutorType } from './types'; import { taskManagerMock } from '../../task_manager/task_manager.mock'; -import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects'; import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/plugin.mock'; const savedObjectsClient = SavedObjectsClientMock.create(); const mockTaskManager = taskManagerMock.create(); -const mockEncryptedSavedObjectsPlugin = { - getDecryptedAsInternalUser: jest.fn() as EncryptedSavedObjectsPlugin['getDecryptedAsInternalUser'], -} as EncryptedSavedObjectsPlugin; +const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); function getServices() { return { @@ -33,6 +31,8 @@ const actionTypeRegistryParams = { getServices, taskManager: mockTaskManager, encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + getBasePath: jest.fn().mockReturnValue(undefined), }; const executor: ExecutorType = async options => { @@ -43,55 +43,56 @@ beforeEach(() => jest.resetAllMocks()); describe('create()', () => { test('creates an action with all given properties', async () => { - const expectedResult = { + const savedObjectCreateResult = { id: '1', type: 'type', - attributes: {}, + attributes: { + description: 'my description', + actionTypeId: 'my-action-type', + config: {}, + }, references: [], }; const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], executor, }); const actionsClient = new ActionsClient({ actionTypeRegistry, savedObjectsClient, }); - savedObjectsClient.create.mockResolvedValueOnce(expectedResult); + savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); const result = await actionsClient.create({ - attributes: { + action: { description: 'my description', actionTypeId: 'my-action-type', - actionTypeConfig: {}, - }, - options: { - migrationVersion: {}, - references: [], + config: {}, + secrets: {}, }, }); - expect(result).toEqual(expectedResult); + expect(result).toEqual({ + id: '1', + description: 'my description', + actionTypeId: 'my-action-type', + config: {}, + }); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", Object { - "actionTypeConfig": Object {}, - "actionTypeConfigSecrets": Object {}, "actionTypeId": "my-action-type", + "config": Object {}, "description": "my description", - }, - Object { - "migrationVersion": Object {}, - "references": Array [], + "secrets": Object {}, }, ] `); }); - test('validates actionTypeConfig', async () => { + test('validates config', async () => { const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); const actionsClient = new ActionsClient({ actionTypeRegistry, @@ -100,7 +101,6 @@ describe('create()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], validate: { config: schema.object({ param1: schema.string(), @@ -110,14 +110,15 @@ describe('create()', () => { }); await expect( actionsClient.create({ - attributes: { + action: { description: 'my description', actionTypeId: 'my-action-type', - actionTypeConfig: {}, + config: {}, + secrets: {}, }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [param1]: expected value of type [string] but got [undefined]"` + `"error validating action type config: [param1]: expected value of type [string] but got [undefined]"` ); }); @@ -129,10 +130,11 @@ describe('create()', () => { }); await expect( actionsClient.create({ - attributes: { + action: { description: 'my description', actionTypeId: 'unregistered-action-type', - actionTypeConfig: {}, + config: {}, + secrets: {}, }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -141,52 +143,67 @@ describe('create()', () => { }); test('encrypts action type options unless specified not to', async () => { - const expectedResult = { - id: '1', - type: 'type', - attributes: {}, - references: [], - }; const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: ['a', 'c'], executor, }); const actionsClient = new ActionsClient({ actionTypeRegistry, savedObjectsClient, }); - savedObjectsClient.create.mockResolvedValueOnce(expectedResult); - const result = await actionsClient.create({ + savedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'type', attributes: { description: 'my description', actionTypeId: 'my-action-type', - actionTypeConfig: { + config: { a: true, b: true, c: true, }, + secrets: {}, + }, + references: [], + }); + const result = await actionsClient.create({ + action: { + description: 'my description', + actionTypeId: 'my-action-type', + config: { + a: true, + b: true, + c: true, + }, + secrets: {}, + }, + }); + expect(result).toEqual({ + id: '1', + description: 'my description', + actionTypeId: 'my-action-type', + config: { + a: true, + b: true, + c: true, }, }); - expect(result).toEqual(expectedResult); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", Object { - "actionTypeConfig": Object { + "actionTypeId": "my-action-type", + "config": Object { "a": true, - "c": true, - }, - "actionTypeConfigSecrets": Object { "b": true, + "c": true, }, - "actionTypeId": "my-action-type", "description": "my description", + "secrets": Object {}, }, - undefined, ] `); }); @@ -194,20 +211,21 @@ describe('create()', () => { describe('get()', () => { test('calls savedObjectsClient with id', async () => { - const expectedResult = { - id: '1', - type: 'type', - attributes: {}, - references: [], - }; const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); const actionsClient = new ActionsClient({ actionTypeRegistry, savedObjectsClient, }); - savedObjectsClient.get.mockResolvedValueOnce(expectedResult); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'type', + attributes: {}, + references: [], + }); const result = await actionsClient.get({ id: '1' }); - expect(result).toEqual(expectedResult); + expect(result).toEqual({ + id: '1', + }); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -228,7 +246,11 @@ describe('find()', () => { { id: '1', type: 'type', - attributes: {}, + attributes: { + config: { + foo: 'bar', + }, + }, references: [], }, ], @@ -240,7 +262,19 @@ describe('find()', () => { }); savedObjectsClient.find.mockResolvedValueOnce(expectedResult); const result = await actionsClient.find({}); - expect(result).toEqual(expectedResult); + expect(result).toEqual({ + total: 1, + perPage: 10, + page: 1, + data: [ + { + id: '1', + config: { + foo: 'bar', + }, + }, + ], + }); expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -275,17 +309,10 @@ describe('delete()', () => { describe('update()', () => { test('updates an action with all given properties', async () => { - const expectedResult = { - id: '1', - type: 'action', - attributes: {}, - references: [], - }; const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], executor, }); const actionsClient = new ActionsClient({ @@ -300,28 +327,42 @@ describe('update()', () => { }, references: [], }); - savedObjectsClient.update.mockResolvedValueOnce(expectedResult); - const result = await actionsClient.update({ + savedObjectsClient.update.mockResolvedValueOnce({ id: 'my-action', + type: 'action', attributes: { + actionTypeId: 'my-action-type', description: 'my description', - actionTypeConfig: {}, + config: {}, + secrets: {}, }, - options: {}, + references: [], + }); + const result = await actionsClient.update({ + id: 'my-action', + action: { + description: 'my description', + config: {}, + secrets: {}, + }, + }); + expect(result).toEqual({ + id: 'my-action', + actionTypeId: 'my-action-type', + description: 'my description', + config: {}, }); - expect(result).toEqual(expectedResult); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", "my-action", Object { - "actionTypeConfig": Object {}, - "actionTypeConfigSecrets": Object {}, "actionTypeId": "my-action-type", + "config": Object {}, "description": "my description", + "secrets": Object {}, }, - Object {}, ] `); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); @@ -333,7 +374,7 @@ describe('update()', () => { `); }); - test('validates actionTypeConfig', async () => { + test('validates config', async () => { const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); const actionsClient = new ActionsClient({ actionTypeRegistry, @@ -342,7 +383,6 @@ describe('update()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: [], validate: { config: schema.object({ param1: schema.string(), @@ -361,29 +401,22 @@ describe('update()', () => { await expect( actionsClient.update({ id: 'my-action', - attributes: { + action: { description: 'my description', - actionTypeConfig: {}, + config: {}, + secrets: {}, }, - options: {}, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [param1]: expected value of type [string] but got [undefined]"` + `"error validating action type config: [param1]: expected value of type [string] but got [undefined]"` ); }); test('encrypts action type options unless specified not to', async () => { - const expectedResult = { - id: '1', - type: 'type', - attributes: {}, - references: [], - }; const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - unencryptedAttributes: ['a', 'c'], executor, }); const actionsClient = new ActionsClient({ @@ -398,37 +431,58 @@ describe('update()', () => { }, references: [], }); - savedObjectsClient.update.mockResolvedValueOnce(expectedResult); - const result = await actionsClient.update({ + savedObjectsClient.update.mockResolvedValueOnce({ id: 'my-action', + type: 'action', attributes: { + actionTypeId: 'my-action-type', description: 'my description', - actionTypeConfig: { + config: { a: true, b: true, c: true, }, + secrets: {}, + }, + references: [], + }); + const result = await actionsClient.update({ + id: 'my-action', + action: { + description: 'my description', + config: { + a: true, + b: true, + c: true, + }, + secrets: {}, + }, + }); + expect(result).toEqual({ + id: 'my-action', + actionTypeId: 'my-action-type', + description: 'my description', + config: { + a: true, + b: true, + c: true, }, - options: {}, }); - expect(result).toEqual(expectedResult); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", "my-action", Object { - "actionTypeConfig": Object { + "actionTypeId": "my-action-type", + "config": Object { "a": true, - "c": true, - }, - "actionTypeConfigSecrets": Object { "b": true, + "c": true, }, - "actionTypeId": "my-action-type", "description": "my description", + "secrets": Object {}, }, - Object {}, ] `); }); diff --git a/x-pack/legacy/plugins/actions/server/actions_client.ts b/x-pack/legacy/plugins/actions/server/actions_client.ts index 4ab62a6e434a..2c5e84d0d5be 100644 --- a/x-pack/legacy/plugins/actions/server/actions_client.ts +++ b/x-pack/legacy/plugins/actions/server/actions_client.ts @@ -4,23 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/server'; +import { SavedObjectsClientContract, SavedObjectAttributes, SavedObject } from 'src/core/server'; import { ActionTypeRegistry } from './action_type_registry'; -import { SavedObjectReference } from './types'; -import { validateActionTypeConfig } from './lib'; +import { validateConfig, validateSecrets } from './lib'; +import { ActionResult } from './types'; -interface Action extends SavedObjectAttributes { +interface ActionUpdate extends SavedObjectAttributes { description: string; + config: SavedObjectAttributes; + secrets: SavedObjectAttributes; +} + +interface Action extends ActionUpdate { actionTypeId: string; - actionTypeConfig: SavedObjectAttributes; } interface CreateOptions { - attributes: Action; - options?: { - migrationVersion?: Record; - references?: SavedObjectReference[]; - }; + action: Action; } interface FindOptions { @@ -39,6 +39,13 @@ interface FindOptions { }; } +interface FindResult { + page: number; + perPage: number; + total: number; + data: ActionResult[]; +} + interface ConstructorOptions { actionTypeRegistry: ActionTypeRegistry; savedObjectsClient: SavedObjectsClientContract; @@ -46,11 +53,7 @@ interface ConstructorOptions { interface UpdateOptions { id: string; - attributes: { - description: string; - actionTypeConfig: SavedObjectAttributes; - }; - options: { version?: string; references?: SavedObjectReference[] }; + action: ActionUpdate; } export class ActionsClient { @@ -65,38 +68,82 @@ export class ActionsClient { /** * Create an action */ - public async create({ attributes, options }: CreateOptions) { - const { actionTypeId } = attributes; + public async create({ action }: CreateOptions): Promise { + const { actionTypeId, description, config, secrets } = action; const actionType = this.actionTypeRegistry.get(actionTypeId); - const validatedActionTypeConfig = validateActionTypeConfig( - actionType, - attributes.actionTypeConfig - ); - const actionWithSplitActionTypeConfig = this.moveEncryptedAttributesToSecrets( - actionType.unencryptedAttributes, - { - ...attributes, - actionTypeConfig: validatedActionTypeConfig, - } - ); - return await this.savedObjectsClient.create('action', actionWithSplitActionTypeConfig, options); + const validatedActionTypeConfig = validateConfig(actionType, config); + const validatedActionTypeSecrets = validateSecrets(actionType, secrets); + + const result = await this.savedObjectsClient.create('action', { + actionTypeId, + description, + config: validatedActionTypeConfig as SavedObjectAttributes, + secrets: validatedActionTypeSecrets as SavedObjectAttributes, + }); + + return { + id: result.id, + actionTypeId: result.attributes.actionTypeId, + description: result.attributes.description, + config: result.attributes.config, + }; + } + + /** + * Update action + */ + public async update({ id, action }: UpdateOptions): Promise { + const existingObject = await this.savedObjectsClient.get('action', id); + const { actionTypeId } = existingObject.attributes; + const { description, config, secrets } = action; + const actionType = this.actionTypeRegistry.get(actionTypeId); + const validatedActionTypeConfig = validateConfig(actionType, config); + const validatedActionTypeSecrets = validateSecrets(actionType, secrets); + + const result = await this.savedObjectsClient.update('action', id, { + actionTypeId, + description, + config: validatedActionTypeConfig as SavedObjectAttributes, + secrets: validatedActionTypeSecrets as SavedObjectAttributes, + }); + + return { + id, + actionTypeId: result.attributes.actionTypeId as string, + description: result.attributes.description as string, + config: result.attributes.config as Record, + }; } /** * Get an action */ - public async get({ id }: { id: string }) { - return await this.savedObjectsClient.get('action', id); + public async get({ id }: { id: string }): Promise { + const result = await this.savedObjectsClient.get('action', id); + + return { + id, + actionTypeId: result.attributes.actionTypeId as string, + description: result.attributes.description as string, + config: result.attributes.config as Record, + }; } /** * Find actions */ - public async find({ options = {} }: FindOptions) { - return await this.savedObjectsClient.find({ + public async find({ options = {} }: FindOptions): Promise { + const findResult = await this.savedObjectsClient.find({ ...options, type: 'action', }); + + return { + page: findResult.page, + perPage: findResult.per_page, + total: findResult.total, + data: findResult.saved_objects.map(actionFromSavedObject), + }; } /** @@ -105,53 +152,11 @@ export class ActionsClient { public async delete({ id }: { id: string }) { return await this.savedObjectsClient.delete('action', id); } +} - /** - * Update action - */ - public async update({ id, attributes, options = {} }: UpdateOptions) { - const existingObject = await this.savedObjectsClient.get('action', id); - const { actionTypeId } = existingObject.attributes; - const actionType = this.actionTypeRegistry.get(actionTypeId); - - const validatedActionTypeConfig = validateActionTypeConfig( - actionType, - attributes.actionTypeConfig - ); - attributes = this.moveEncryptedAttributesToSecrets(actionType.unencryptedAttributes, { - ...attributes, - actionTypeConfig: validatedActionTypeConfig, - }); - return await this.savedObjectsClient.update( - 'action', - id, - { - ...attributes, - actionTypeId, - }, - options - ); - } - - /** - * Set actionTypeConfigSecrets values on a given action - */ - private moveEncryptedAttributesToSecrets( - unencryptedAttributes: string[] = [], - action: Action | UpdateOptions['attributes'] - ) { - const actionTypeConfig: Record = {}; - const actionTypeConfigSecrets = { ...action.actionTypeConfig }; - for (const attributeKey of unencryptedAttributes) { - actionTypeConfig[attributeKey] = actionTypeConfigSecrets[attributeKey]; - delete actionTypeConfigSecrets[attributeKey]; - } - - return { - ...action, - // Important these overwrite attributes for encryption purposes - actionTypeConfig, - actionTypeConfigSecrets, - }; - } +function actionFromSavedObject(savedObject: SavedObject) { + return { + id: savedObject.id, + ...savedObject.attributes, + }; } diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts index 5bfcb8d211dd..7ebb1ff94375 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.test.ts @@ -12,11 +12,11 @@ import { ActionType, ActionTypeExecutorOptions } from '../types'; import { ActionTypeRegistry } from '../action_type_registry'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; import { taskManagerMock } from '../../../task_manager/task_manager.mock'; -import { validateActionTypeConfig, validateActionTypeParams } from '../lib'; +import { validateParams, validateConfig, validateSecrets } from '../lib'; import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { registerBuiltInActionTypes } from './index'; import { sendEmail } from './lib/send_email'; -import { ActionParamsType, ActionTypeConfigType } from './email'; +import { ActionParamsType, ActionTypeConfigType, ActionTypeSecretsType } from './email'; const sendEmailMock = sendEmail as jest.Mock; @@ -43,6 +43,8 @@ beforeAll(() => { getServices, taskManager: taskManagerMock.create(), encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + getBasePath: jest.fn().mockReturnValue(undefined), }); registerBuiltInActionTypes(actionTypeRegistry); @@ -71,11 +73,9 @@ describe('config validation', () => { test('config validation succeeds when config is valid', () => { const config: Record = { service: 'gmail', - user: 'bob', - password: 'supersecret', from: 'bob@example.com', }; - expect(validateActionTypeConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config)).toEqual({ ...config, host: null, port: null, @@ -85,7 +85,7 @@ describe('config validation', () => { delete config.service; config.host = 'elastic.co'; config.port = 8080; - expect(validateActionTypeConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config)).toEqual({ ...config, service: null, secure: null, @@ -101,37 +101,58 @@ describe('config validation', () => { // empty object expect(() => { - validateActionTypeConfig(actionType, {}); + validateConfig(actionType, {}); }).toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [user]: expected value of type [string] but got [undefined]"` + `"error validating action type config: [from]: expected value of type [string] but got [undefined]"` ); // no service or host/port expect(() => { - validateActionTypeConfig(actionType, baseConfig); + validateConfig(actionType, baseConfig); }).toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: either [service] or [host]/[port] is required"` + `"error validating action type config: [user]: definition for this key is missing"` ); // host but no port expect(() => { - validateActionTypeConfig(actionType, { ...baseConfig, host: 'elastic.co' }); + validateConfig(actionType, { ...baseConfig, host: 'elastic.co' }); }).toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [port] is required if [service] is not provided"` + `"error validating action type config: [user]: definition for this key is missing"` ); // port but no host expect(() => { - validateActionTypeConfig(actionType, { ...baseConfig, port: 8080 }); + validateConfig(actionType, { ...baseConfig, port: 8080 }); }).toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [host] is required if [service] is not provided"` + `"error validating action type config: [user]: definition for this key is missing"` ); // invalid service expect(() => { - validateActionTypeConfig(actionType, { ...baseConfig, service: 'bad-nodemailer-service' }); + validateConfig(actionType, { + ...baseConfig, + service: 'bad-nodemailer-service', + }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [user]: definition for this key is missing"` + ); + }); +}); + +describe('secrets validation', () => { + test('secrets validation succeeds when secrets is valid', () => { + const secrets: Record = { + user: 'bob', + password: 'supersecret', + }; + expect(validateSecrets(actionType, secrets)).toEqual(secrets); + }); + + test('secrets validation fails when secrets is not valid', () => { + expect(() => { + validateSecrets(actionType, {}); }).toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [service] value \\"bad-nodemailer-service\\" is not valid"` + `"error validating action type secrets: [user]: expected value of type [string] but got [undefined]"` ); }); }); @@ -143,7 +164,7 @@ describe('params validation', () => { subject: 'this is a test', message: 'this is the message', }; - expect(validateActionTypeParams(actionType, params)).toMatchInlineSnapshot(` + expect(validateParams(actionType, params)).toMatchInlineSnapshot(` Object { "bcc": Array [], "cc": Array [], @@ -159,9 +180,9 @@ Object { test('params validation fails when params is not valid', () => { // empty object expect(() => { - validateActionTypeParams(actionType, {}); + validateParams(actionType, {}); }).toThrowErrorMatchingInlineSnapshot( - `"The actionParams is invalid: [subject]: expected value of type [string] but got [undefined]"` + `"error validating action params: [subject]: expected value of type [string] but got [undefined]"` ); }); }); @@ -173,9 +194,11 @@ describe('execute()', () => { host: 'a host', port: 42, secure: true, + from: 'bob@example.com', + }; + const secrets: ActionTypeSecretsType = { user: 'bob', password: 'supersecret', - from: 'bob@example.com', }; const params: ActionParamsType = { to: ['jim@example.com'], @@ -185,7 +208,8 @@ describe('execute()', () => { message: 'a message to you', }; - const executorOptions: ActionTypeExecutorOptions = { config, params, services }; + const id = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { id, config, params, secrets, services }; sendEmailMock.mockReset(); await actionType.executor(executorOptions); expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts index 7d8df0e35314..a2d3aa58656a 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts @@ -4,28 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf, Type } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import nodemailerServices from 'nodemailer/lib/well-known/services.json'; import { sendEmail, JSON_TRANSPORT_SERVICE } from './lib/send_email'; +import { nullableType } from './lib/nullable'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; const PORT_MAX = 256 * 256 - 1; -function nullableType(type: Type) { - return schema.oneOf([type, schema.literal(null)], { defaultValue: () => null }); -} - // config definition -const unencryptedConfigProperties = ['service', 'host', 'port', 'secure', 'from']; - export type ActionTypeConfigType = TypeOf; const ConfigSchema = schema.object( { - user: schema.string(), - password: schema.string(), service: nullableType(schema.string()), host: nullableType(schema.string()), port: nullableType(schema.number({ min: 1, max: PORT_MAX })), @@ -63,6 +56,15 @@ function validateConfig(configObject: any): string | void { } } +// secrets definition + +export type ActionTypeSecretsType = TypeOf; + +const SecretsSchema = schema.object({ + user: schema.string(), + password: schema.string(), +}); + // params definition export type ActionParamsType = TypeOf; @@ -97,9 +99,9 @@ function validateParams(paramsObject: any): string | void { export const actionType: ActionType = { id: '.email', name: 'email', - unencryptedAttributes: unencryptedConfigProperties, validate: { config: ConfigSchema, + secrets: SecretsSchema, params: ParamsSchema, }, executor, @@ -108,13 +110,15 @@ export const actionType: ActionType = { // action executor async function executor(execOptions: ActionTypeExecutorOptions): Promise { + const id = execOptions.id; const config = execOptions.config as ActionTypeConfigType; + const secrets = execOptions.secrets as ActionTypeSecretsType; const params = execOptions.params as ActionParamsType; const services = execOptions.services; const transport: any = { - user: config.user, - password: config.password, + user: secrets.user, + password: secrets.password, }; if (config.service !== null) { @@ -146,7 +150,7 @@ async function executor(execOptions: ActionTypeExecutorOptions): Promise ({ + sendEmail: jest.fn(), +})); + +import { ActionType, ActionTypeExecutorOptions } from '../types'; +import { ActionTypeRegistry } from '../action_type_registry'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; +import { taskManagerMock } from '../../../task_manager/task_manager.mock'; +import { validateConfig, validateParams } from '../lib'; +import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { registerBuiltInActionTypes } from './index'; +import { ActionParamsType, ActionTypeConfigType } from './es_index'; + +const ACTION_TYPE_ID = '.index'; +const NO_OP_FN = () => {}; + +const services = { + log: NO_OP_FN, + callCluster: jest.fn(), + savedObjectsClient: SavedObjectsClientMock.create(), +}; + +function getServices() { + return services; +} + +let actionTypeRegistry: ActionTypeRegistry; +let actionType: ActionType; + +const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); + +beforeAll(() => { + actionTypeRegistry = new ActionTypeRegistry({ + getServices, + taskManager: taskManagerMock.create(), + encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + getBasePath: jest.fn().mockReturnValue(undefined), + }); + + registerBuiltInActionTypes(actionTypeRegistry); + + actionType = actionTypeRegistry.get(ACTION_TYPE_ID); +}); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('action is registered', () => { + test('gets registered with builtin actions', () => { + expect(actionTypeRegistry.has(ACTION_TYPE_ID)).toEqual(true); + }); +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionType.id).toEqual(ACTION_TYPE_ID); + expect(actionType.name).toEqual('index'); + }); +}); + +describe('config validation', () => { + test('config validation succeeds when config is valid', () => { + const config: Record = {}; + + expect(validateConfig(actionType, config)).toEqual({ + ...config, + index: null, + }); + + config.index = 'testing-123'; + expect(validateConfig(actionType, config)).toEqual({ + ...config, + index: 'testing-123', + }); + }); + + test('config validation fails when config is not valid', () => { + const baseConfig: Record = { + indeX: 'bob', + }; + + expect(() => { + validateConfig(actionType, baseConfig); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [indeX]: definition for this key is missing"` + ); + + delete baseConfig.user; + baseConfig.index = 666; + + expect(() => { + validateConfig(actionType, baseConfig); + }).toThrowErrorMatchingInlineSnapshot(` +"error validating action type config: [index]: types that failed validation: +- [index.0]: expected value of type [string] but got [number] +- [index.1]: expected value to equal [null] but got [666]" +`); + }); +}); + +describe('params validation', () => { + test('params validation succeeds when params is valid', () => { + const params: Record = { + index: 'testing-123', + executionTimeField: 'field-used-for-time', + refresh: true, + documents: [{ rando: 'thing' }], + }; + expect(validateParams(actionType, params)).toMatchInlineSnapshot(` + Object { + "documents": Array [ + Object { + "rando": "thing", + }, + ], + "executionTimeField": "field-used-for-time", + "index": "testing-123", + "refresh": true, + } + `); + + delete params.index; + delete params.refresh; + delete params.executionTimeField; + expect(validateParams(actionType, params)).toMatchInlineSnapshot(` + Object { + "documents": Array [ + Object { + "rando": "thing", + }, + ], + } + `); + }); + + test('params validation fails when params is not valid', () => { + expect(() => { + validateParams(actionType, { documents: [{}], jim: 'bob' }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [jim]: definition for this key is missing"` + ); + + expect(() => { + validateParams(actionType, {}); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [documents]: expected value of type [array] but got [undefined]"` + ); + + expect(() => { + validateParams(actionType, { index: 666 }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [index]: expected value of type [string] but got [number]"` + ); + + expect(() => { + validateParams(actionType, { executionTimeField: true }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [executionTimeField]: expected value of type [string] but got [boolean]"` + ); + + expect(() => { + validateParams(actionType, { refresh: 'true' }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [refresh]: expected value of type [boolean] but got [string]"` + ); + + expect(() => { + validateParams(actionType, { documents: ['should be an object'] }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [documents.0]: expected value of type [object] but got [string]"` + ); + }); +}); + +describe('execute()', () => { + test('ensure parameters are as expected', async () => { + const secrets = {}; + let config: ActionTypeConfigType; + let params: ActionParamsType; + let executorOptions: ActionTypeExecutorOptions; + + // minimal params, index via param + config = { index: null }; + params = { + index: 'index-via-param', + documents: [{ jim: 'bob' }], + executionTimeField: undefined, + refresh: undefined, + }; + + const id = 'some-id'; + + executorOptions = { id, config, secrets, params, services }; + services.callCluster.mockClear(); + await actionType.executor(executorOptions); + + expect(services.callCluster.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "bulk", + Object { + "body": Array [ + Object { + "index": Object {}, + }, + Object { + "jim": "bob", + }, + ], + "index": "index-via-param", + }, + ], + ] + `); + + // full params (except index), index via config + config = { index: 'index-via-config' }; + params = { + index: undefined, + documents: [{ jimbob: 'jr' }], + executionTimeField: 'field_to_use_for_time', + refresh: true, + }; + + executorOptions = { id, config, secrets, params, services }; + services.callCluster.mockClear(); + await actionType.executor(executorOptions); + + const calls = services.callCluster.mock.calls; + const timeValue = calls[0][1].body[1].field_to_use_for_time; + expect(timeValue).toBeInstanceOf(Date); + delete calls[0][1].body[1].field_to_use_for_time; + expect(calls).toMatchInlineSnapshot(` + Array [ + Array [ + "bulk", + Object { + "body": Array [ + Object { + "index": Object {}, + }, + Object { + "jimbob": "jr", + }, + ], + "index": "index-via-config", + "refresh": true, + }, + ], + ] + `); + + // minimal params, index via config and param + config = { index: 'index-via-config' }; + params = { + index: 'index-via-param', + documents: [{ jim: 'bob' }], + executionTimeField: undefined, + refresh: undefined, + }; + + executorOptions = { id, config, secrets, params, services }; + services.callCluster.mockClear(); + await actionType.executor(executorOptions); + + expect(services.callCluster.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "bulk", + Object { + "body": Array [ + Object { + "index": Object {}, + }, + Object { + "jim": "bob", + }, + ], + "index": "index-via-config", + }, + ], + ] + `); + + // multiple documents + config = { index: null }; + params = { + index: 'index-via-param', + documents: [{ a: 1 }, { b: 2 }], + executionTimeField: undefined, + refresh: undefined, + }; + + executorOptions = { id, config, secrets, params, services }; + services.callCluster.mockClear(); + await actionType.executor(executorOptions); + + expect(services.callCluster.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "bulk", + Object { + "body": Array [ + Object { + "index": Object {}, + }, + Object { + "a": 1, + }, + Object { + "index": Object {}, + }, + Object { + "b": 2, + }, + ], + "index": "index-via-param", + }, + ], + ] + `); + }); +}); diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts new file mode 100644 index 000000000000..4d32633f541b --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/es_index.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, TypeOf } from '@kbn/config-schema'; + +import { nullableType } from './lib/nullable'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; + +// config definition + +export type ActionTypeConfigType = TypeOf; + +const ConfigSchema = schema.object({ + index: nullableType(schema.string()), +}); + +// params definition + +export type ActionParamsType = TypeOf; + +// see: https://www.elastic.co/guide/en/elastic-stack-overview/current/actions-index.html +// - timeout not added here, as this seems to be a generic thing we want to do +// eventually: https://github.com/elastic/kibana/projects/26#card-24087404 +const ParamsSchema = schema.object({ + index: schema.maybe(schema.string()), + executionTimeField: schema.maybe(schema.string()), + refresh: schema.maybe(schema.boolean()), + documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), +}); + +// action type definition + +export const actionType: ActionType = { + id: '.index', + name: 'index', + validate: { + config: ConfigSchema, + params: ParamsSchema, + }, + executor, +}; + +// action executor + +async function executor(execOptions: ActionTypeExecutorOptions): Promise { + const id = execOptions.id; + const config = execOptions.config as ActionTypeConfigType; + const params = execOptions.params as ActionParamsType; + const services = execOptions.services; + + if (config.index == null && params.index == null) { + return { + status: 'error', + message: `index param needs to be set because not set in config for action ${id}`, + }; + } + + if (config.index != null && params.index != null) { + services.log( + ['debug', 'actions'], + `index passed in params overridden by index set in config for action ${id}` + ); + } + + const index = config.index || params.index; + + const bulkBody = []; + for (const document of params.documents) { + if (params.executionTimeField != null) { + document[params.executionTimeField] = new Date(); + } + + bulkBody.push({ index: {} }); + bulkBody.push(document); + } + + const bulkParams: any = { + index, + body: bulkBody, + }; + + if (params.refresh != null) { + bulkParams.refresh = params.refresh; + } + + let result; + try { + result = await services.callCluster('bulk', bulkParams); + } catch (err) { + return { + status: 'error', + message: `error in action ${id} indexing documents: ${err.message}`, + }; + } + + return { status: 'ok', data: result }; +} diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/index.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/index.ts index 1b59a6438120..ea0597933618 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/index.ts @@ -9,9 +9,11 @@ import { ActionTypeRegistry } from '../action_type_registry'; import { actionType as serverLogActionType } from './server_log'; import { actionType as slackActionType } from './slack'; import { actionType as emailActionType } from './email'; +import { actionType as indexActionType } from './es_index'; export function registerBuiltInActionTypes(actionTypeRegistry: ActionTypeRegistry) { actionTypeRegistry.register(serverLogActionType); actionTypeRegistry.register(slackActionType); actionTypeRegistry.register(emailActionType); + actionTypeRegistry.register(indexActionType); } diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/nullable.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/nullable.ts new file mode 100644 index 000000000000..e2ea1005ca18 --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/lib/nullable.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, Type } from '@kbn/config-schema'; + +// TODO: remove once this is merged: https://github.com/elastic/kibana/pull/41728 + +export function nullableType(type: Type) { + return schema.oneOf([type, schema.literal(null)], { defaultValue: () => null }); +} diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts index 83f5cf06b795..ef38c5b91062 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -8,7 +8,7 @@ import { ActionType, Services } from '../types'; import { ActionTypeRegistry } from '../action_type_registry'; import { taskManagerMock } from '../../../task_manager/task_manager.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock'; -import { validateActionTypeParams } from '../lib'; +import { validateParams } from '../lib'; import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { registerBuiltInActionTypes } from './index'; @@ -35,6 +35,8 @@ beforeAll(() => { getServices, taskManager: taskManagerMock.create(), encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + getBasePath: jest.fn().mockReturnValue(undefined), }); registerBuiltInActionTypes(actionTypeRegistry); }); @@ -57,7 +59,7 @@ describe('get()', () => { }); }); -describe('validateActionTypeParams()', () => { +describe('validateParams()', () => { let actionType: ActionType; beforeAll(() => { @@ -66,12 +68,12 @@ describe('validateActionTypeParams()', () => { }); test('should validate and pass when params is valid', () => { - expect(validateActionTypeParams(actionType, { message: 'a message' })).toEqual({ + expect(validateParams(actionType, { message: 'a message' })).toEqual({ message: 'a message', tags: ['info', 'alerting'], }); expect( - validateActionTypeParams(actionType, { + validateParams(actionType, { message: 'a message', tags: ['info', 'blorg'], }) @@ -83,27 +85,27 @@ describe('validateActionTypeParams()', () => { test('should validate and throw error when params is invalid', () => { expect(() => { - validateActionTypeParams(actionType, {}); + validateParams(actionType, {}); }).toThrowErrorMatchingInlineSnapshot( - `"The actionParams is invalid: [message]: expected value of type [string] but got [undefined]"` + `"error validating action params: [message]: expected value of type [string] but got [undefined]"` ); expect(() => { - validateActionTypeParams(actionType, { message: 1 }); + validateParams(actionType, { message: 1 }); }).toThrowErrorMatchingInlineSnapshot( - `"The actionParams is invalid: [message]: expected value of type [string] but got [number]"` + `"error validating action params: [message]: expected value of type [string] but got [number]"` ); expect(() => { - validateActionTypeParams(actionType, { message: 'x', tags: 2 }); + validateParams(actionType, { message: 'x', tags: 2 }); }).toThrowErrorMatchingInlineSnapshot( - `"The actionParams is invalid: [tags]: expected value of type [array] but got [number]"` + `"error validating action params: [tags]: expected value of type [array] but got [number]"` ); expect(() => { - validateActionTypeParams(actionType, { message: 'x', tags: [2] }); + validateParams(actionType, { message: 'x', tags: [2] }); }).toThrowErrorMatchingInlineSnapshot( - `"The actionParams is invalid: [tags.0]: expected value of type [string] but got [number]"` + `"error validating action params: [tags.0]: expected value of type [string] but got [number]"` ); }); }); @@ -114,14 +116,17 @@ describe('execute()', () => { services.log = mockLog; const actionType = actionTypeRegistry.get(ACTION_TYPE_ID); + const id = 'some-id'; await actionType.executor({ + id, services: { log: mockLog, callCluster: async (path: string, opts: any) => {}, savedObjectsClient: SavedObjectsClientMock.create(), }, - config: {}, params: { message: 'message text here', tags: ['tag1', 'tag2'] }, + config: {}, + secrets: {}, }); expect(mockLog).toMatchInlineSnapshot(` [MockFunction] { diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts index 426dbb606744..a11362ff6371 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/server_log.ts @@ -10,12 +10,6 @@ import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from const DEFAULT_TAGS = ['info', 'alerting']; -// config definition - -const unencryptedConfigProperties: string[] = []; - -const ConfigSchema = schema.object({}); - // params definition export type ActionParamsType = TypeOf; @@ -30,9 +24,7 @@ const ParamsSchema = schema.object({ export const actionType: ActionType = { id: '.server-log', name: 'server-log', - unencryptedAttributes: unencryptedConfigProperties, validate: { - config: ConfigSchema, params: ParamsSchema, }, executor, @@ -41,6 +33,7 @@ export const actionType: ActionType = { // action executor async function executor(execOptions: ActionTypeExecutorOptions): Promise { + const id = execOptions.id; const params = execOptions.params as ActionParamsType; const services = execOptions.services; @@ -49,7 +42,7 @@ async function executor(execOptions: ActionTypeExecutorOptions): Promise { getServices, taskManager: taskManagerMock.create(), encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + getBasePath: jest.fn().mockReturnValue(undefined), }); actionTypeRegistry.register(getActionType({ executor: mockSlackExecutor })); actionType = actionTypeRegistry.get(ACTION_TYPE_ID); @@ -76,44 +77,44 @@ describe('action is registered', () => { describe('validateParams()', () => { test('should validate and pass when params is valid', () => { - expect(validateActionTypeParams(actionType, { message: 'a message' })).toEqual({ + expect(validateParams(actionType, { message: 'a message' })).toEqual({ message: 'a message', }); }); test('should validate and throw error when params is invalid', () => { expect(() => { - validateActionTypeParams(actionType, {}); + validateParams(actionType, {}); }).toThrowErrorMatchingInlineSnapshot( - `"The actionParams is invalid: [message]: expected value of type [string] but got [undefined]"` + `"error validating action params: [message]: expected value of type [string] but got [undefined]"` ); expect(() => { - validateActionTypeParams(actionType, { message: 1 }); + validateParams(actionType, { message: 1 }); }).toThrowErrorMatchingInlineSnapshot( - `"The actionParams is invalid: [message]: expected value of type [string] but got [number]"` + `"error validating action params: [message]: expected value of type [string] but got [number]"` ); }); }); -describe('validateActionTypeConfig()', () => { +describe('validateActionTypeSecrets()', () => { test('should validate and pass when config is valid', () => { - validateActionTypeConfig(actionType, { + validateSecrets(actionType, { webhookUrl: 'https://example.com', }); }); test('should validate and throw error when config is invalid', () => { expect(() => { - validateActionTypeConfig(actionType, {}); + validateSecrets(actionType, {}); }).toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [webhookUrl]: expected value of type [string] but got [undefined]"` + `"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [undefined]"` ); expect(() => { - validateActionTypeConfig(actionType, { webhookUrl: 1 }); + validateSecrets(actionType, { webhookUrl: 1 }); }).toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [webhookUrl]: expected value of type [string] but got [number]"` + `"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [number]"` ); }); }); @@ -121,8 +122,10 @@ describe('validateActionTypeConfig()', () => { describe('execute()', () => { test('calls the mock executor with success', async () => { const response = await actionType.executor({ + id: 'some-id', services, - config: { webhookUrl: 'http://example.com' }, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, }); expect(response).toMatchInlineSnapshot(` @@ -135,8 +138,10 @@ Object { test('calls the mock executor with failure', async () => { await expect( actionType.executor({ + id: 'some-id', services, - config: { webhookUrl: 'http://example.com' }, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, params: { message: 'failure: this invocation should fail' }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.ts index e077c06305bd..a5bd254a2083 100644 --- a/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/legacy/plugins/actions/server/builtin_action_types/slack.ts @@ -14,13 +14,11 @@ import { ExecutorType, } from '../types'; -// config definition +// secrets definition -const unencryptedConfigProperties: string[] = []; +export type ActionTypeSecretsType = TypeOf; -export type ActionTypeConfigType = TypeOf; - -const ConfigSchema = schema.object({ +const SecretsSchema = schema.object({ webhookUrl: schema.string(), }); @@ -41,9 +39,8 @@ export function getActionType({ executor }: { executor?: ExecutorType } = {}): A return { id: '.slack', name: 'slack', - unencryptedAttributes: unencryptedConfigProperties, validate: { - config: ConfigSchema, + secrets: SecretsSchema, params: ParamsSchema, }, executor, @@ -58,11 +55,12 @@ export const actionType = getActionType(); async function slackExecutor( execOptions: ActionTypeExecutorOptions ): Promise { - const config = execOptions.config as ActionTypeConfigType; + const id = execOptions.id; + const secrets = execOptions.secrets as ActionTypeSecretsType; const params = execOptions.params as ActionParamsType; let result: IncomingWebhookResult; - const { webhookUrl } = config; + const { webhookUrl } = secrets; const { message } = params; try { @@ -70,14 +68,14 @@ async function slackExecutor( result = await webhook.send(message); } catch (err) { if (err.original == null || err.original.response == null) { - return errorResult(err.message); + return errorResult(id, err.message); } const { status, statusText, headers } = err.original.response; // special handling for 5xx if (status >= 500) { - return retryResult(err.message); + return retryResult(id, err.message); } // special handling for rate limiting @@ -86,20 +84,20 @@ async function slackExecutor( if (retryAfterString != null) { const retryAfter = parseInt(retryAfterString, 10); if (!isNaN(retryAfter)) { - return retryResultSeconds(err.message, retryAfter); + return retryResultSeconds(id, err.message, retryAfter); } } } - return errorResult(`${err.message} - ${statusText}`); + return errorResult(id, `${err.message} - ${statusText}`); } if (result == null) { - return errorResult(`unexpected null response from slack`); + return errorResult(id, `unexpected null response from slack`); } if (result.text !== 'ok') { - return errorResult(`unexpected text response from slack (expecting 'ok')`); + return errorResult(id, `unexpected text response from slack (expecting 'ok')`); } return successResult(result); @@ -109,28 +107,32 @@ function successResult(data: any): ActionTypeExecutorResult { return { status: 'ok', data }; } -function errorResult(message: string): ActionTypeExecutorResult { +function errorResult(id: string, message: string): ActionTypeExecutorResult { return { status: 'error', - message: `an error occurred posting a slack message: ${message}`, + message: `an error occurred in action ${id} posting a slack message: ${message}`, }; } -function retryResult(message: string): ActionTypeExecutorResult { +function retryResult(id: string, message: string): ActionTypeExecutorResult { return { status: 'error', - message: `an error occurred posting a slack message, retrying later`, + message: `an error occurred in action ${id} posting a slack message, retrying later`, retry: true, }; } -function retryResultSeconds(message: string, retryAfter: number = 60): ActionTypeExecutorResult { +function retryResultSeconds( + id: string, + message: string, + retryAfter: number = 60 +): ActionTypeExecutorResult { const retryEpoch = Date.now() + retryAfter * 1000; const retry = new Date(retryEpoch); const retryString = retry.toISOString(); return { status: 'error', - message: `an error occurred posting a slack message, retry at ${retryString}: ${message}`, + message: `an error occurred in action ${id} posting a slack message, retry at ${retryString}: ${message}`, retry, }; } diff --git a/x-pack/legacy/plugins/actions/server/create_fire_function.test.ts b/x-pack/legacy/plugins/actions/server/create_fire_function.test.ts index 8def316c12df..1f286d371e63 100644 --- a/x-pack/legacy/plugins/actions/server/create_fire_function.test.ts +++ b/x-pack/legacy/plugins/actions/server/create_fire_function.test.ts @@ -10,6 +10,7 @@ import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks'; const mockTaskManager = taskManagerMock.create(); const savedObjectsClient = SavedObjectsClientMock.create(); +const spaceIdToNamespace = jest.fn(); beforeEach(() => jest.resetAllMocks()); @@ -18,6 +19,7 @@ describe('fire()', () => { const fireFn = createFireFunction({ taskManager: mockTaskManager, internalSavedObjectsRepository: savedObjectsClient, + spaceIdToNamespace, }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -27,41 +29,41 @@ describe('fire()', () => { }, references: [], }); + spaceIdToNamespace.mockReturnValueOnce('namespace1'); await fireFn({ id: '123', params: { baz: false }, - namespace: 'abc', - basePath: '/s/default', + spaceId: 'default', }); expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` -Array [ - Object { - "params": Object { - "actionTypeParams": Object { - "baz": false, - }, - "basePath": "/s/default", - "id": "123", - "namespace": "abc", - }, - "scope": Array [ - "actions", - ], - "state": Object {}, - "taskType": "actions:mock-action", - }, -] -`); + Array [ + Object { + "params": Object { + "id": "123", + "params": Object { + "baz": false, + }, + "spaceId": "default", + }, + "scope": Array [ + "actions", + ], + "state": Object {}, + "taskType": "actions:mock-action", + }, + ] + `); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` -Array [ - "action", - "123", - Object { - "namespace": "abc", - }, -] -`); + Array [ + "action", + "123", + Object { + "namespace": "namespace1", + }, + ] + `); + expect(spaceIdToNamespace).toHaveBeenCalledWith('default'); }); }); diff --git a/x-pack/legacy/plugins/actions/server/create_fire_function.ts b/x-pack/legacy/plugins/actions/server/create_fire_function.ts index 7ff31f4d455f..0bf11cbf5dd4 100644 --- a/x-pack/legacy/plugins/actions/server/create_fire_function.ts +++ b/x-pack/legacy/plugins/actions/server/create_fire_function.ts @@ -6,32 +6,34 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { TaskManager } from '../../task_manager'; +import { SpacesPlugin } from '../../spaces'; interface CreateFireFunctionOptions { taskManager: TaskManager; internalSavedObjectsRepository: SavedObjectsClientContract; + spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; } -interface FireOptions { +export interface FireOptions { id: string; params: Record; - namespace?: string; - basePath: string; + spaceId: string; } export function createFireFunction({ taskManager, internalSavedObjectsRepository, + spaceIdToNamespace, }: CreateFireFunctionOptions) { - return async function fire({ id, params, namespace, basePath }: FireOptions) { + return async function fire({ id, params, spaceId }: FireOptions) { + const namespace = spaceIdToNamespace(spaceId); const actionSavedObject = await internalSavedObjectsRepository.get('action', id, { namespace }); await taskManager.schedule({ taskType: `actions:${actionSavedObject.attributes.actionTypeId}`, params: { id, - basePath, - namespace, - actionTypeParams: params, + spaceId, + params, }, state: {}, scope: ['actions'], diff --git a/x-pack/legacy/plugins/actions/server/init.ts b/x-pack/legacy/plugins/actions/server/init.ts index 765653f0fec8..27cff53bb97d 100644 --- a/x-pack/legacy/plugins/actions/server/init.ts +++ b/x-pack/legacy/plugins/actions/server/init.ts @@ -18,19 +18,31 @@ import { listActionTypesRoute, fireRoute, } from './routes'; - import { registerBuiltInActionTypes } from './builtin_action_types'; +import { SpacesPlugin } from '../../spaces'; +import { createOptionalPlugin } from '../../../server/lib/optional_plugin'; export function init(server: Legacy.Server) { + const config = server.config(); + const spaces = createOptionalPlugin( + config, + 'xpack.spaces', + server.plugins, + 'spaces' + ); + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); const savedObjectsRepositoryWithInternalUser = server.savedObjects.getSavedObjectsRepository( callWithInternalUser ); // Encrypted attributes + // - `secrets` properties will be encrypted + // - `config` will be included in AAD + // - everything else excluded from AAD server.plugins.encrypted_saved_objects!.registerType({ type: 'action', - attributesToEncrypt: new Set(['actionTypeConfigSecrets']), + attributesToEncrypt: new Set(['secrets']), attributesToExcludeFromAAD: new Set(['description']), }); @@ -55,6 +67,14 @@ export function init(server: Legacy.Server) { getServices, taskManager: taskManager!, encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!, + getBasePath(...args) { + return spaces.isEnabled + ? spaces.getBasePath(...args) + : server.config().get('server.basePath'); + }, + spaceIdToNamespace(...args) { + return spaces.isEnabled ? spaces.spaceIdToNamespace(...args) : undefined; + }, }); registerBuiltInActionTypes(actionTypeRegistry); @@ -75,6 +95,9 @@ export function init(server: Legacy.Server) { const fireFn = createFireFunction({ taskManager: taskManager!, internalSavedObjectsRepository: savedObjectsRepositoryWithInternalUser, + spaceIdToNamespace(...args) { + return spaces.isEnabled ? spaces.spaceIdToNamespace(...args) : undefined; + }, }); // Expose functions to server diff --git a/x-pack/legacy/plugins/actions/server/lib/execute.test.ts b/x-pack/legacy/plugins/actions/server/lib/execute.test.ts index 73a5566474a2..0f14df6e8c67 100644 --- a/x-pack/legacy/plugins/actions/server/lib/execute.test.ts +++ b/x-pack/legacy/plugins/actions/server/lib/execute.test.ts @@ -39,7 +39,6 @@ test('successfully executes', async () => { const actionType = { id: 'test', name: 'Test', - unencryptedAttributes: [], executor: jest.fn(), }; const actionSavedObject = { @@ -47,10 +46,10 @@ test('successfully executes', async () => { type: 'action', attributes: { actionTypeId: 'test', - actionTypeConfig: { + config: { bar: true, }, - actionTypeConfigSecrets: { + secrets: { baz: true, }, }, @@ -70,20 +69,22 @@ test('successfully executes', async () => { expect(actionTypeRegistry.get).toHaveBeenCalledWith('test'); expect(actionType.executor).toHaveBeenCalledWith({ + id: '1', services: expect.anything(), config: { bar: true, + }, + secrets: { baz: true, }, params: { foo: true }, }); }); -test('provides empty config when actionTypeConfig and / or actionTypeConfigSecrets is empty', async () => { +test('provides empty config when config and / or secrets is empty', async () => { const actionType = { id: 'test', name: 'Test', - unencryptedAttributes: [], executor: jest.fn(), }; const actionSavedObject = { @@ -101,14 +102,13 @@ test('provides empty config when actionTypeConfig and / or actionTypeConfigSecre expect(actionType.executor).toHaveBeenCalledTimes(1); const executorCall = actionType.executor.mock.calls[0][0]; - expect(executorCall.config).toMatchInlineSnapshot(`Object {}`); + expect(executorCall.config).toMatchInlineSnapshot(`undefined`); }); test('throws an error when config is invalid', async () => { const actionType = { id: 'test', name: 'Test', - unencryptedAttributes: [], validate: { config: schema.object({ param1: schema.string(), @@ -128,16 +128,18 @@ test('throws an error when config is invalid', async () => { encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); - await expect(execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [param1]: expected value of type [string] but got [undefined]"` - ); + const result = await execute(executeParams); + expect(result).toEqual({ + status: 'error', + retry: false, + message: `error validating action type config: [param1]: expected value of type [string] but got [undefined]`, + }); }); test('throws an error when params is invalid', async () => { const actionType = { id: 'test', name: 'Test', - unencryptedAttributes: [], validate: { params: schema.object({ param1: schema.string(), @@ -157,7 +159,10 @@ test('throws an error when params is invalid', async () => { encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); - await expect(execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot( - `"The actionParams is invalid: [param1]: expected value of type [string] but got [undefined]"` - ); + const result = await execute(executeParams); + expect(result).toEqual({ + status: 'error', + retry: false, + message: `error validating action params: [param1]: expected value of type [string] but got [undefined]`, + }); }); diff --git a/x-pack/legacy/plugins/actions/server/lib/execute.ts b/x-pack/legacy/plugins/actions/server/lib/execute.ts index e75425e0cae6..95c4f17dd922 100644 --- a/x-pack/legacy/plugins/actions/server/lib/execute.ts +++ b/x-pack/legacy/plugins/actions/server/lib/execute.ts @@ -5,13 +5,12 @@ */ import { Services, ActionTypeRegistryContract, ActionTypeExecutorResult } from '../types'; -import { validateActionTypeConfig } from './validate_action_type_config'; -import { validateActionTypeParams } from './validate_action_type_params'; +import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects'; interface ExecuteOptions { actionId: string; - namespace: string; + namespace?: string; services: Services; params: Record; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; @@ -31,12 +30,18 @@ export async function execute({ namespace, }); const actionType = actionTypeRegistry.get(action.attributes.actionTypeId); - const mergedActionTypeConfig = { - ...(action.attributes.actionTypeConfig || {}), - ...(action.attributes.actionTypeConfigSecrets || {}), - }; - const validatedConfig = validateActionTypeConfig(actionType, mergedActionTypeConfig); - const validatedParams = validateActionTypeParams(actionType, params); + + let validatedParams; + let validatedConfig; + let validatedSecrets; + + try { + validatedParams = validateParams(actionType, params); + validatedConfig = validateConfig(actionType, action.attributes.config); + validatedSecrets = validateSecrets(actionType, action.attributes.secrets); + } catch (err) { + return { status: 'error', message: err.message, retry: false }; + } let result: ActionTypeExecutorResult | null = null; @@ -45,9 +50,11 @@ export async function execute({ try { result = await actionType.executor({ + id: actionId, services, - config: validatedConfig, params: validatedParams, + config: validatedConfig, + secrets: validatedSecrets, }); } catch (err) { services.log( diff --git a/x-pack/legacy/plugins/actions/server/lib/executor_error.ts b/x-pack/legacy/plugins/actions/server/lib/executor_error.ts new file mode 100644 index 000000000000..5e0dee3f3cc2 --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/lib/executor_error.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class ExecutorError extends Error { + readonly data?: any; + readonly retry?: null | boolean | Date; + constructor(message?: string, data?: any, retry?: null | boolean | Date) { + super(message); + this.data = data; + this.retry = retry; + } +} diff --git a/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.test.ts b/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.test.ts index 489325748b4d..4f732afc3ec7 100644 --- a/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.test.ts +++ b/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.test.ts @@ -12,14 +12,15 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv import { getCreateTaskRunnerFunction } from './get_create_task_runner_function'; import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { actionTypeRegistryMock } from '../action_type_registry.mock'; +import { ExecutorError } from './executor_error'; +const spaceIdToNamespace = jest.fn(); const actionTypeRegistry = actionTypeRegistryMock.create(); const mockedEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create(); const actionType = { id: '1', name: '1', - unencryptedAttributes: [], executor: jest.fn(), }; @@ -34,7 +35,9 @@ const getCreateTaskRunnerFunctionParams = { }; }, actionTypeRegistry, + spaceIdToNamespace, encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, + getBasePath: jest.fn().mockReturnValue(undefined), }; const taskInstanceMock = { @@ -42,8 +45,8 @@ const taskInstanceMock = { state: {}, params: { id: '2', - actionTypeParams: { baz: true }, - namespace: 'test', + params: { baz: true }, + spaceId: 'test', }, taskType: 'actions:1', }; @@ -54,11 +57,16 @@ test('executes the task by calling the executor with proper parameters', async ( const { execute: mockExecute } = jest.requireMock('./execute'); const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams); const runner = createTaskRunner({ taskInstance: taskInstanceMock }); + + mockExecute.mockResolvedValueOnce({ status: 'ok' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + const runnerResult = await runner.run(); expect(runnerResult).toBeUndefined(); + expect(spaceIdToNamespace).toHaveBeenCalledWith('test'); expect(mockExecute).toHaveBeenCalledWith({ - namespace: 'test', + namespace: 'namespace-test', actionId: '2', actionTypeRegistry, encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, @@ -66,3 +74,25 @@ test('executes the task by calling the executor with proper parameters', async ( params: { baz: true }, }); }); + +test('throws an error with suggested retry logic when return status is error', async () => { + const { execute: mockExecute } = jest.requireMock('./execute'); + const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams); + const runner = createTaskRunner({ taskInstance: taskInstanceMock }); + + mockExecute.mockResolvedValueOnce({ + status: 'error', + message: 'Error message', + data: { foo: true }, + retry: false, + }); + + try { + await runner.run(); + throw new Error('Should have thrown'); + } catch (e) { + expect(e instanceof ExecutorError).toEqual(true); + expect(e.data).toEqual({ foo: true }); + expect(e.retry).toEqual(false); + } +}); diff --git a/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.ts b/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.ts index 04b7345781e3..f9398f25ff7f 100644 --- a/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.ts +++ b/x-pack/legacy/plugins/actions/server/lib/get_create_task_runner_function.ts @@ -5,14 +5,18 @@ */ import { execute } from './execute'; +import { ExecutorError } from './executor_error'; import { ActionTypeRegistryContract, GetServicesFunction } from '../types'; import { TaskInstance } from '../../../task_manager'; import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects'; +import { SpacesPlugin } from '../../../spaces'; interface CreateTaskRunnerFunctionOptions { getServices: GetServicesFunction; actionTypeRegistry: ActionTypeRegistryContract; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin; + spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; + getBasePath: SpacesPlugin['getBasePath']; } interface TaskRunnerOptions { @@ -23,19 +27,32 @@ export function getCreateTaskRunnerFunction({ getServices, actionTypeRegistry, encryptedSavedObjectsPlugin, + spaceIdToNamespace, + getBasePath, }: CreateTaskRunnerFunctionOptions) { return ({ taskInstance }: TaskRunnerOptions) => { return { run: async () => { - const { namespace, id, actionTypeParams } = taskInstance.params; - await execute({ + const { spaceId, id, params } = taskInstance.params; + const namespace = spaceIdToNamespace(spaceId); + const basePath = getBasePath(spaceId); + const executorResult = await execute({ namespace, actionTypeRegistry, encryptedSavedObjectsPlugin, actionId: id, - services: getServices(taskInstance.params.basePath), - params: actionTypeParams, + services: getServices(basePath), + params, }); + if (executorResult.status === 'error') { + // Task manager error handler only kicks in when an error thrown (at this time) + // So what we have to do is throw when the return status is `error`. + throw new ExecutorError( + executorResult.message, + executorResult.data, + executorResult.retry + ); + } }, }; }; diff --git a/x-pack/legacy/plugins/actions/server/lib/index.ts b/x-pack/legacy/plugins/actions/server/lib/index.ts index 23305f4eba90..c1cca1f68add 100644 --- a/x-pack/legacy/plugins/actions/server/lib/index.ts +++ b/x-pack/legacy/plugins/actions/server/lib/index.ts @@ -6,5 +6,5 @@ export { execute } from './execute'; export { getCreateTaskRunnerFunction } from './get_create_task_runner_function'; -export { validateActionTypeConfig } from './validate_action_type_config'; -export { validateActionTypeParams } from './validate_action_type_params'; +export { ExecutorError } from './executor_error'; +export { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; diff --git a/x-pack/legacy/plugins/actions/server/lib/validate_action_type_config.test.ts b/x-pack/legacy/plugins/actions/server/lib/validate_action_type_config.test.ts deleted file mode 100644 index b348384d6e52..000000000000 --- a/x-pack/legacy/plugins/actions/server/lib/validate_action_type_config.test.ts +++ /dev/null @@ -1,75 +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 { schema } from '@kbn/config-schema'; -import { validateActionTypeConfig } from './validate_action_type_config'; -import { ExecutorType } from '../types'; - -const executor: ExecutorType = async options => { - return { status: 'ok' }; -}; - -test('should return passed in config when validation not defined', () => { - const result = validateActionTypeConfig( - { - id: 'my-action-type', - name: 'My action type', - unencryptedAttributes: [], - executor, - }, - { - foo: true, - } - ); - expect(result).toEqual({ foo: true }); -}); - -test('should validate and apply defaults when actionTypeConfig is valid', () => { - const result = validateActionTypeConfig( - { - id: 'my-action-type', - name: 'My action type', - unencryptedAttributes: [], - validate: { - config: schema.object({ - param1: schema.string(), - param2: schema.string({ defaultValue: 'default-value' }), - }), - }, - executor, - }, - { param1: 'value' } - ); - expect(result).toEqual({ - param1: 'value', - param2: 'default-value', - }); -}); - -test('should validate and throw error when actionTypeConfig is invalid', () => { - expect(() => - validateActionTypeConfig( - { - id: 'my-action-type', - name: 'My action type', - unencryptedAttributes: [], - validate: { - config: schema.object({ - obj: schema.object({ - param1: schema.string(), - }), - }), - }, - executor, - }, - { - obj: {}, - } - ) - ).toThrowErrorMatchingInlineSnapshot( - `"The actionTypeConfig is invalid: [obj.param1]: expected value of type [string] but got [undefined]"` - ); -}); diff --git a/x-pack/legacy/plugins/actions/server/lib/validate_action_type_config.ts b/x-pack/legacy/plugins/actions/server/lib/validate_action_type_config.ts deleted file mode 100644 index ce8bc7dba2a9..000000000000 --- a/x-pack/legacy/plugins/actions/server/lib/validate_action_type_config.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { ActionType } from '../types'; - -export function validateActionTypeConfig>( - actionType: ActionType, - config: T -): T { - const validator = actionType.validate && actionType.validate.config; - if (!validator) { - return config; - } - - try { - return validator.validate(config); - } catch (err) { - throw Boom.badRequest(`The actionTypeConfig is invalid: ${err.message}`); - } -} diff --git a/x-pack/legacy/plugins/actions/server/lib/validate_action_type_params.test.ts b/x-pack/legacy/plugins/actions/server/lib/validate_action_type_params.test.ts deleted file mode 100644 index 58de8c01d89b..000000000000 --- a/x-pack/legacy/plugins/actions/server/lib/validate_action_type_params.test.ts +++ /dev/null @@ -1,73 +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 { schema } from '@kbn/config-schema'; -import { validateActionTypeParams } from './validate_action_type_params'; -import { ExecutorType } from '../types'; - -const executor: ExecutorType = async options => { - return { status: 'ok' }; -}; - -test('should return passed in params when validation not defined', () => { - const result = validateActionTypeParams( - { - id: 'my-action-type', - name: 'My action type', - unencryptedAttributes: [], - executor, - }, - { - foo: true, - } - ); - expect(result).toEqual({ - foo: true, - }); -}); - -test('should validate and apply defaults when params is valid', () => { - const result = validateActionTypeParams( - { - id: 'my-action-type', - name: 'My action type', - unencryptedAttributes: [], - validate: { - params: schema.object({ - param1: schema.string(), - param2: schema.string({ defaultValue: 'default-value' }), - }), - }, - executor, - }, - { param1: 'value' } - ); - expect(result).toEqual({ - param1: 'value', - param2: 'default-value', - }); -}); - -test('should validate and throw error when params is invalid', () => { - expect(() => - validateActionTypeParams( - { - id: 'my-action-type', - name: 'My action type', - unencryptedAttributes: [], - validate: { - params: schema.object({ - param1: schema.string(), - }), - }, - executor, - }, - {} - ) - ).toThrowErrorMatchingInlineSnapshot( - `"The actionParams is invalid: [param1]: expected value of type [string] but got [undefined]"` - ); -}); diff --git a/x-pack/legacy/plugins/actions/server/lib/validate_action_type_params.ts b/x-pack/legacy/plugins/actions/server/lib/validate_action_type_params.ts deleted file mode 100644 index 4d18c27d79fa..000000000000 --- a/x-pack/legacy/plugins/actions/server/lib/validate_action_type_params.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { ActionType } from '../types'; - -export function validateActionTypeParams>( - actionType: ActionType, - params: T -): T { - const validator = actionType.validate && actionType.validate.params; - if (!validator) { - return params; - } - try { - return validator.validate(params); - } catch (err) { - throw Boom.badRequest(`The actionParams is invalid: ${err.message}`); - } -} diff --git a/x-pack/legacy/plugins/actions/server/lib/validate_with_schema.test.ts b/x-pack/legacy/plugins/actions/server/lib/validate_with_schema.test.ts new file mode 100644 index 000000000000..4cb28728fb42 --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/lib/validate_with_schema.test.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; +import { ActionType, ExecutorType } from '../types'; + +const executor: ExecutorType = async options => { + return { status: 'ok' }; +}; + +test('should validate when there are no validators', () => { + const actionType: ActionType = { id: 'foo', name: 'bar', executor }; + const testValue = { any: ['old', 'thing'] }; + + const result = validateConfig(actionType, testValue); + expect(result).toEqual(testValue); +}); + +test('should validate when there are no individual validators', () => { + const actionType: ActionType = { id: 'foo', name: 'bar', executor, validate: {} }; + + let result; + const testValue = { any: ['old', 'thing'] }; + + result = validateParams(actionType, testValue); + expect(result).toEqual(testValue); + + result = validateConfig(actionType, testValue); + expect(result).toEqual(testValue); + + result = validateSecrets(actionType, testValue); + expect(result).toEqual(testValue); +}); + +test('should validate when validators return incoming value', () => { + const selfValidator = { validate: (value: any) => value }; + const actionType: ActionType = { + id: 'foo', + name: 'bar', + executor, + validate: { + params: selfValidator, + config: selfValidator, + secrets: selfValidator, + }, + }; + + let result; + const testValue = { any: ['old', 'thing'] }; + + result = validateParams(actionType, testValue); + expect(result).toEqual(testValue); + + result = validateConfig(actionType, testValue); + expect(result).toEqual(testValue); + + result = validateSecrets(actionType, testValue); + expect(result).toEqual(testValue); +}); + +test('should validate when validators return different values', () => { + const returnedValue: any = { something: { shaped: 'differently' } }; + const selfValidator = { validate: (value: any) => returnedValue }; + const actionType: ActionType = { + id: 'foo', + name: 'bar', + executor, + validate: { + params: selfValidator, + config: selfValidator, + secrets: selfValidator, + }, + }; + + let result; + const testValue = { any: ['old', 'thing'] }; + + result = validateParams(actionType, testValue); + expect(result).toEqual(returnedValue); + + result = validateConfig(actionType, testValue); + expect(result).toEqual(returnedValue); + + result = validateSecrets(actionType, testValue); + expect(result).toEqual(returnedValue); +}); + +test('should throw with expected error when validators fail', () => { + const erroringValidator = { + validate: (value: any) => { + throw new Error('test error'); + }, + }; + const actionType: ActionType = { + id: 'foo', + name: 'bar', + executor, + validate: { + params: erroringValidator, + config: erroringValidator, + secrets: erroringValidator, + }, + }; + + const testValue = { any: ['old', 'thing'] }; + + expect(() => validateParams(actionType, testValue)).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: test error"` + ); + + expect(() => validateConfig(actionType, testValue)).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: test error"` + ); + + expect(() => validateSecrets(actionType, testValue)).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: test error"` + ); +}); + +test('should work with @kbn/config-schema', () => { + const testSchema = schema.object({ foo: schema.string() }); + const actionType: ActionType = { + id: 'foo', + name: 'bar', + executor, + validate: { + params: testSchema, + config: testSchema, + secrets: testSchema, + }, + }; + + const result = validateParams(actionType, { foo: 'bar' }); + expect(result).toEqual({ foo: 'bar' }); + + expect(() => validateParams(actionType, { bar: 2 })).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [foo]: expected value of type [string] but got [undefined]"` + ); +}); diff --git a/x-pack/legacy/plugins/actions/server/lib/validate_with_schema.ts b/x-pack/legacy/plugins/actions/server/lib/validate_with_schema.ts new file mode 100644 index 000000000000..45ef867834ea --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/lib/validate_with_schema.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 Boom from 'boom'; +import { ActionType } from '../types'; + +export function validateParams(actionType: ActionType, value: any) { + return validateWithSchema(actionType, 'params', value); +} + +export function validateConfig(actionType: ActionType, value: any) { + return validateWithSchema(actionType, 'config', value); +} + +export function validateSecrets(actionType: ActionType, value: any) { + return validateWithSchema(actionType, 'secrets', value); +} + +type ValidKeys = 'params' | 'config' | 'secrets'; + +function validateWithSchema( + actionType: ActionType, + key: ValidKeys, + value: any +): Record { + if (actionType.validate == null) return value; + + let name; + try { + switch (key) { + case 'params': + name = 'action params'; + if (actionType.validate.params == null) return value; + return actionType.validate.params.validate(value); + + case 'config': + name = 'action type config'; + if (actionType.validate.config == null) return value; + return actionType.validate.config.validate(value); + + case 'secrets': + name = 'action type secrets'; + if (actionType.validate.secrets == null) return value; + return actionType.validate.secrets.validate(value); + } + } catch (err) { + throw Boom.badRequest(`error validating ${name}: ${err.message}`); + } + + // should never happen, but left here for future-proofing + throw new Error(`invalid actionType validate key: ${key}`); +} diff --git a/x-pack/legacy/plugins/actions/server/routes/create.test.ts b/x-pack/legacy/plugins/actions/server/routes/create.test.ts index f1e93c46b80e..442af6b88d16 100644 --- a/x-pack/legacy/plugins/actions/server/routes/create.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/create.test.ts @@ -19,73 +19,42 @@ it('creates an action with proper parameters', async () => { method: 'POST', url: '/api/action', payload: { - attributes: { - description: 'My description', - actionTypeId: 'abc', - actionTypeConfig: { foo: true }, - }, - migrationVersion: { - abc: '1.2.3', - }, - references: [ - { - name: 'ref_0', - type: 'bcd', - id: '234', - }, - ], + description: 'My description', + actionTypeId: 'abc', + config: { foo: true }, + secrets: {}, }, }; const createResult = { id: '1', - type: 'action', - attributes: { - description: 'My description', - actionTypeId: 'abc', - actionTypeConfig: { foo: true }, - actionTypeConfigSecrets: {}, - }, - migrationVersion: { - abc: '1.2.3', - }, - references: [ - { - name: 'ref_0', - type: 'bcd', - id: '234', - }, - ], + description: 'My description', + actionTypeId: 'abc', + config: { foo: true }, }; actionsClient.create.mockResolvedValueOnce(createResult); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); const response = JSON.parse(payload); - expect(response).toEqual({ id: '1' }); + expect(response).toEqual({ + id: '1', + description: 'My description', + actionTypeId: 'abc', + config: { foo: true }, + }); expect(actionsClient.create).toHaveBeenCalledTimes(1); expect(actionsClient.create.mock.calls[0]).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object { - "actionTypeConfig": Object { - "foo": true, - }, - "actionTypeId": "abc", - "description": "My description", - }, - "options": Object { - "migrationVersion": Object { - "abc": "1.2.3", - }, - "references": Array [ - Object { - "id": "234", - "name": "ref_0", - "type": "bcd", + Array [ + Object { + "action": Object { + "actionTypeId": "abc", + "config": Object { + "foo": true, + }, + "description": "My description", + "secrets": Object {}, }, - ], - }, - }, -] -`); + }, + ] + `); }); diff --git a/x-pack/legacy/plugins/actions/server/routes/create.ts b/x-pack/legacy/plugins/actions/server/routes/create.ts index 0cba5816879f..e1367cdf15ba 100644 --- a/x-pack/legacy/plugins/actions/server/routes/create.ts +++ b/x-pack/legacy/plugins/actions/server/routes/create.ts @@ -6,7 +6,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { WithoutQueryAndParams, SavedObjectReference } from '../types'; +import { ActionResult, WithoutQueryAndParams } from '../types'; interface CreateRequest extends WithoutQueryAndParams { query: { @@ -16,13 +16,10 @@ interface CreateRequest extends WithoutQueryAndParams { id?: string; }; payload: { - attributes: { - description: string; - actionTypeId: string; - actionTypeConfig: Record; - }; - migrationVersion?: Record; - references: SavedObjectReference[]; + description: string; + actionTypeId: string; + config: Record; + secrets: Record; }; } @@ -35,39 +32,21 @@ export function createRoute(server: Hapi.Server) { options: { abortEarly: false, }, - payload: Joi.object().keys({ - attributes: Joi.object() - .keys({ - description: Joi.string().required(), - actionTypeId: Joi.string().required(), - actionTypeConfig: Joi.object().required(), - }) - .required(), - migrationVersion: Joi.object().optional(), - references: Joi.array() - .items( - Joi.object().keys({ - name: Joi.string().required(), - type: Joi.string().required(), - id: Joi.string().required(), - }) - ) - .default([]), - }), + payload: Joi.object() + .keys({ + description: Joi.string().required(), + actionTypeId: Joi.string().required(), + config: Joi.object().default({}), + secrets: Joi.object().default({}), + }) + .required(), }, }, - async handler(request: CreateRequest) { + async handler(request: CreateRequest): Promise { const actionsClient = request.getActionsClient!(); - const createdAction = await actionsClient.create({ - attributes: request.payload.attributes, - options: { - migrationVersion: request.payload.migrationVersion, - references: request.payload.references, - }, - }); - - return { id: createdAction.id }; + const action = request.payload; + return await actionsClient.create({ action }); }, }); } diff --git a/x-pack/legacy/plugins/actions/server/routes/delete.test.ts b/x-pack/legacy/plugins/actions/server/routes/delete.test.ts index 37c7f3b2c89a..a655b804f397 100644 --- a/x-pack/legacy/plugins/actions/server/routes/delete.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/delete.test.ts @@ -22,9 +22,8 @@ it('deletes an action with proper parameters', async () => { actionsClient.delete.mockResolvedValueOnce({ success: true }); const { payload, statusCode } = await server.inject(request); - expect(statusCode).toBe(200); - const response = JSON.parse(payload); - expect(response).toEqual({ success: true }); + expect(statusCode).toBe(204); + expect(payload).toEqual(''); expect(actionsClient.delete).toHaveBeenCalledTimes(1); expect(actionsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/legacy/plugins/actions/server/routes/delete.ts b/x-pack/legacy/plugins/actions/server/routes/delete.ts index eed8b7a10cde..6a47b4395d9c 100644 --- a/x-pack/legacy/plugins/actions/server/routes/delete.ts +++ b/x-pack/legacy/plugins/actions/server/routes/delete.ts @@ -26,10 +26,11 @@ export function deleteRoute(server: Hapi.Server) { .required(), }, }, - async handler(request: DeleteRequest) { + async handler(request: DeleteRequest, h: Hapi.ResponseToolkit) { const { id } = request.params; const actionsClient = request.getActionsClient!(); - return await actionsClient.delete({ id }); + await actionsClient.delete({ id }); + return h.response().code(204); }, }); } diff --git a/x-pack/legacy/plugins/actions/server/routes/find.test.ts b/x-pack/legacy/plugins/actions/server/routes/find.test.ts index afb8f583e541..f2073a906e7e 100644 --- a/x-pack/legacy/plugins/actions/server/routes/find.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/find.test.ts @@ -29,9 +29,9 @@ it('sends proper arguments to action find function', async () => { }; const expectedResult = { total: 0, - per_page: 10, + perPage: 10, page: 1, - saved_objects: [], + data: [], }; actionsClient.find.mockResolvedValueOnce(expectedResult); diff --git a/x-pack/legacy/plugins/actions/server/routes/get.test.ts b/x-pack/legacy/plugins/actions/server/routes/get.test.ts index bec51eff1e80..8d1949774445 100644 --- a/x-pack/legacy/plugins/actions/server/routes/get.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/get.test.ts @@ -6,6 +6,7 @@ import { createMockServer } from './_mock_server'; import { getRoute } from './get'; +import { ActionResult } from '../types'; const { server, actionsClient } = createMockServer(); getRoute(server); @@ -19,11 +20,11 @@ it('calls get with proper parameters', async () => { method: 'GET', url: '/api/action/1', }; - const expectedResult = { + const expectedResult: ActionResult = { id: '1', - type: 'action', - attributes: {}, - references: [], + actionTypeId: 'my-action-type-id', + config: {}, + description: 'my action type description', }; actionsClient.get.mockResolvedValueOnce(expectedResult); diff --git a/x-pack/legacy/plugins/actions/server/routes/update.test.ts b/x-pack/legacy/plugins/actions/server/routes/update.test.ts index 492542d0190b..a08522ccb981 100644 --- a/x-pack/legacy/plugins/actions/server/routes/update.test.ts +++ b/x-pack/legacy/plugins/actions/server/routes/update.test.ts @@ -6,6 +6,7 @@ import { createMockServer } from './_mock_server'; import { updateRoute } from './update'; +import { ActionResult } from '../types'; const { server, actionsClient } = createMockServer(); updateRoute(server); @@ -19,64 +20,40 @@ it('calls the update function with proper parameters', async () => { method: 'PUT', url: '/api/action/1', payload: { - attributes: { - description: 'My description', - actionTypeConfig: { foo: true }, - }, - version: '2', - references: [ - { - name: 'ref_0', - type: 'bcd', - id: '234', - }, - ], + description: 'My description', + config: { foo: true }, }, }; - const updateResult = { + const updateResult: ActionResult = { id: '1', - type: 'action', - attributes: { - description: 'My description', - actionTypeConfig: { foo: true }, - }, - version: '2', - references: [ - { - name: 'ref_0', - type: 'bcd', - id: '234', - }, - ], + actionTypeId: 'my-action-type-id', + description: 'My description', + config: { foo: true }, }; actionsClient.update.mockResolvedValueOnce(updateResult); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); const response = JSON.parse(payload); - expect(response).toEqual({ id: '1' }); + expect(response).toEqual({ + id: '1', + actionTypeId: 'my-action-type-id', + description: 'My description', + config: { foo: true }, + }); expect(actionsClient.update).toHaveBeenCalledTimes(1); expect(actionsClient.update.mock.calls[0]).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object { - "actionTypeConfig": Object { - "foo": true, - }, - "description": "My description", - }, - "id": "1", - "options": Object { - "references": Array [ - Object { - "id": "234", - "name": "ref_0", - "type": "bcd", + Array [ + Object { + "action": Object { + "config": Object { + "foo": true, + }, + "description": "My description", + "secrets": Object {}, }, - ], - "version": "2", - }, - }, -] -`); + "id": "1", + }, + ] + `); }); diff --git a/x-pack/legacy/plugins/actions/server/routes/update.ts b/x-pack/legacy/plugins/actions/server/routes/update.ts index 68931c21225a..f8c2e7059f78 100644 --- a/x-pack/legacy/plugins/actions/server/routes/update.ts +++ b/x-pack/legacy/plugins/actions/server/routes/update.ts @@ -7,17 +7,11 @@ import Joi from 'joi'; import Hapi from 'hapi'; -import { SavedObjectReference } from '../types'; - interface UpdateRequest extends Hapi.Request { payload: { - attributes: { - description: string; - actionTypeId: string; - actionTypeConfig: Record; - }; - version?: string; - references: SavedObjectReference[]; + description: string; + config: Record; + secrets: Record; }; } @@ -37,37 +31,18 @@ export function updateRoute(server: Hapi.Server) { .required(), payload: Joi.object() .keys({ - attributes: Joi.object() - .keys({ - description: Joi.string().required(), - actionTypeConfig: Joi.object().required(), - }) - .required(), - version: Joi.string(), - references: Joi.array() - .items( - Joi.object().keys({ - name: Joi.string().required(), - type: Joi.string().required(), - id: Joi.string().required(), - }) - ) - .default([]), + description: Joi.string().required(), + config: Joi.object().default({}), + secrets: Joi.object().default({}), }) .required(), }, }, async handler(request: UpdateRequest) { const { id } = request.params; - const { attributes, version, references } = request.payload; - const options = { version, references }; + const { description, config, secrets } = request.payload; const actionsClient = request.getActionsClient!(); - await actionsClient.update({ - id, - attributes, - options, - }); - return { id }; + return await actionsClient.update({ id, action: { description, config, secrets } }); }, }); } diff --git a/x-pack/legacy/plugins/actions/server/types.ts b/x-pack/legacy/plugins/actions/server/types.ts index 3999370d7acc..a3134b4cd4d1 100644 --- a/x-pack/legacy/plugins/actions/server/types.ts +++ b/x-pack/legacy/plugins/actions/server/types.ts @@ -6,17 +6,12 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { ActionTypeRegistry } from './action_type_registry'; +import { FireOptions } from './create_fire_function'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (basePath: string, overwrites?: Partial) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; -export interface SavedObjectReference { - name: string; - type: string; - id: string; -} - export interface Services { callCluster(path: string, opts: any): Promise; savedObjectsClient: SavedObjectsClientContract; @@ -26,16 +21,25 @@ export interface Services { export interface ActionsPlugin { registerType: ActionTypeRegistry['register']; listTypes: ActionTypeRegistry['list']; - fire(options: { id: string; params: Record; basePath: string }): Promise; + fire(options: FireOptions): Promise; } // the parameters passed to an action type executor function export interface ActionTypeExecutorOptions { + id: string; services: Services; config: Record; + secrets: Record; params: Record; } +export interface ActionResult { + id: string; + actionTypeId: string; + description: string; + config: Record; +} + // the result returned from an action type executor function export interface ActionTypeExecutorResult { status: 'ok' | 'error'; @@ -49,13 +53,18 @@ export type ExecutorType = ( options: ActionTypeExecutorOptions ) => Promise; +interface ValidatorType { + validate(value: any): any; +} + export interface ActionType { id: string; name: string; - unencryptedAttributes: string[]; + maxAttempts?: number; validate?: { - params?: { validate: (object: any) => any }; - config?: { validate: (object: any) => any }; + params?: ValidatorType; + config?: ValidatorType; + secrets?: ValidatorType; }; executor: ExecutorType; } diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 091d5a9da011..18034807b7ac 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -52,8 +52,8 @@ This is the primary function for an alert type. Whenever the alert needs to exec |services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.

    **NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR.| |services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

    **NOTE**: This currently only works when security is disabled. A future PR will add support for enabled security using Elasticsearch API tokens.| |services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| -|scheduledRunAt|The date and time the alert type execution was scheduled to be called.| -|previousScheduledRunAt|The previous date and time the alert type was scheduled to be called.| +|startedAt|The date and time the alert type started execution.| +|previousStartedAt|The previous date and time the alert type started a successful execution.| |params|Parameters for the execution. This is where the parameters you require will be passed in. (example threshold). Use alert type validation to ensure values are set before execution.| |state|State returned from previous execution. This is the alert level state. What the executor returns will be serialized and provided here at the next execution.| @@ -74,8 +74,8 @@ server.plugins.alerting.registerType({ }), }, async executor({ - scheduledRunAt, - previousScheduledRunAt, + startedAt, + previousStartedAt, services, params, state, @@ -131,8 +131,8 @@ server.plugins.alerting.registerType({ }), }, async executor({ - scheduledRunAt, - previousScheduledRunAt, + startedAt, + previousStartedAt, services, params, state, diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts index 442023951714..f14da9e9ed83 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts @@ -25,6 +25,8 @@ const alertTypeRegistryParams = { taskManager, fireAction: jest.fn(), internalSavedObjectsRepository: SavedObjectsClientMock.create(), + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + getBasePath: jest.fn().mockReturnValue(undefined), }; beforeEach(() => jest.resetAllMocks()); @@ -46,7 +48,7 @@ describe('has()', () => { }); }); -describe('registry()', () => { +describe('register()', () => { test('registers the executor with the task manager', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { getCreateTaskRunnerFunction } = require('./lib/get_create_task_runner_function'); @@ -79,6 +81,8 @@ Object { } `); expect(firstCall.internalSavedObjectsRepository).toBeTruthy(); + expect(firstCall.getBasePath).toBeTruthy(); + expect(firstCall.spaceIdToNamespace).toBeTruthy(); expect(firstCall.fireAction).toMatchInlineSnapshot(`[MockFunction]`); }); diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts index 61a358473ef4..7e763858fd20 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts @@ -11,12 +11,15 @@ import { AlertType, Services } from './types'; import { TaskManager } from '../../task_manager'; import { getCreateTaskRunnerFunction } from './lib'; import { ActionsPlugin } from '../../actions'; +import { SpacesPlugin } from '../../spaces'; interface ConstructorOptions { getServices: (basePath: string) => Services; taskManager: TaskManager; fireAction: ActionsPlugin['fire']; internalSavedObjectsRepository: SavedObjectsClientContract; + spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; + getBasePath: SpacesPlugin['getBasePath']; } export class AlertTypeRegistry { @@ -25,17 +28,23 @@ export class AlertTypeRegistry { private readonly fireAction: ActionsPlugin['fire']; private readonly alertTypes: Map = new Map(); private readonly internalSavedObjectsRepository: SavedObjectsClientContract; + private readonly spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; + private readonly getBasePath: SpacesPlugin['getBasePath']; constructor({ internalSavedObjectsRepository, fireAction, taskManager, getServices, + spaceIdToNamespace, + getBasePath, }: ConstructorOptions) { this.taskManager = taskManager; this.fireAction = fireAction; this.internalSavedObjectsRepository = internalSavedObjectsRepository; this.getServices = getServices; + this.getBasePath = getBasePath; + this.spaceIdToNamespace = spaceIdToNamespace; } public has(id: string) { @@ -63,6 +72,8 @@ export class AlertTypeRegistry { getServices: this.getServices, fireAction: this.fireAction, internalSavedObjectsRepository: this.internalSavedObjectsRepository, + getBasePath: this.getBasePath, + spaceIdToNamespace: this.spaceIdToNamespace, }), }, }); diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 2041ab7cfcef..c6eae114e725 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -19,7 +19,7 @@ const alertsClientParams = { taskManager, alertTypeRegistry, savedObjectsClient, - basePath: '/s/default', + spaceId: 'default', }; beforeEach(() => jest.resetAllMocks()); @@ -94,12 +94,12 @@ describe('create()', () => { taskManager.schedule.mockResolvedValueOnce({ id: 'task-123', taskType: 'alerting:123', - sequenceNumber: 1, - primaryTerm: 1, scheduledAt: new Date(), attempts: 1, status: 'idle', runAt: new Date(), + startedAt: null, + retryAt: null, state: {}, params: {}, }); @@ -119,99 +119,98 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "id": "1", + "interval": "10s", + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); expect(savedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "enabled": true, - "interval": "10s", - } - `); + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "enabled": true, + "interval": "10s", + } + `); expect(savedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - } - `); + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + } + `); expect(taskManager.schedule).toHaveBeenCalledTimes(1); expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "alertId": "1", - "basePath": "/s/default", - }, - "scope": Array [ - "alerting", - ], - "state": Object { - "alertInstances": Object {}, - "alertTypeState": Object {}, - "previousScheduledRunAt": null, - "scheduledRunAt": 2019-02-12T21:01:22.479Z, - }, - "taskType": "alerting:123", - }, - ] - `); + Array [ + Object { + "params": Object { + "alertId": "1", + "spaceId": "default", + }, + "scope": Array [ + "alerting", + ], + "state": Object { + "alertInstances": Object {}, + "alertTypeState": Object {}, + "previousStartedAt": null, + }, + "taskType": "alerting:123", + }, + ] + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1'); expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "scheduledTaskId": "task-123", - } - `); + Object { + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - } - `); + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + } + `); }); test('creates a disabled alert', async () => { @@ -252,25 +251,25 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "enabled": false, - "id": "1", - "interval": 10000, - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "enabled": false, + "id": "1", + "interval": 10000, + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); @@ -351,11 +350,11 @@ describe('create()', () => { ); expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1); expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); + Array [ + "alert", + "1", + ] + `); }); test('returns task manager error if cleanup fails, logs to console', async () => { @@ -400,14 +399,14 @@ describe('create()', () => { ); expect(alertsClientParams.log).toHaveBeenCalledTimes(1); expect(alertsClientParams.log.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Array [ - "alerting", - "error", - ], - "Failed to cleanup alert \\"1\\" after scheduling task failed. Error: Saved object delete error", - ] - `); + Array [ + Array [ + "alerting", + "error", + ], + "Failed to cleanup alert \\"1\\" after scheduling task failed. Error: Saved object delete error", + ] + `); }); test('throws an error if alert type not registerd', async () => { @@ -437,8 +436,6 @@ describe('enable()', () => { }); taskManager.schedule.mockResolvedValueOnce({ id: 'task-123', - sequenceNumber: 1, - primaryTerm: 1, scheduledAt: new Date(), attempts: 0, status: 'idle', @@ -446,6 +443,8 @@ describe('enable()', () => { state: {}, params: {}, taskType: '', + startedAt: null, + retryAt: null, }); await alertsClient.enable({ id: '1' }); @@ -464,13 +463,12 @@ describe('enable()', () => { taskType: `alerting:2`, params: { alertId: '1', - basePath: '/s/default', + spaceId: 'default', }, state: { alertInstances: {}, alertTypeState: {}, - previousScheduledRunAt: null, - scheduledRunAt: mockedDate, + previousStartedAt: null, }, scope: ['alerting'], }); @@ -577,31 +575,31 @@ describe('get()', () => { }); const result = await alertsClient.get({ id: '1' }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "id": "1", + "interval": "10s", + } + `); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); + Array [ + "alert", + "1", + ] + `); }); test(`throws an error when references aren't found`, async () => { @@ -672,34 +670,39 @@ describe('find()', () => { }); const result = await alertsClient.find(); expect(result).toMatchInlineSnapshot(` - Array [ + Object { + "data": Array [ + Object { + "actions": Array [ Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, + "group": "default", "id": "1", - "interval": "10s", + "params": Object { + "foo": true, + }, }, - ] - `); + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "id": "1", + "interval": "10s", + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "type": "alert", - }, - ] - `); + Array [ + Object { + "type": "alert", + }, + ] + `); }); }); @@ -737,32 +740,21 @@ describe('delete()', () => { savedObjectsClient.delete.mockResolvedValueOnce({ success: true, }); - taskManager.remove.mockResolvedValueOnce({ - index: '.task_manager', - id: 'task-123', - sequenceNumber: 1, - primaryTerm: 1, - result: '', - }); const result = await alertsClient.delete({ id: '1' }); - expect(result).toMatchInlineSnapshot(` - Object { - "success": true, - } - `); + expect(result).toEqual({ success: true }); expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1); expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); + Array [ + "alert", + "1", + ] + `); expect(taskManager.remove).toHaveBeenCalledTimes(1); expect(taskManager.remove.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "task-123", - ] - `); + Array [ + "task-123", + ] + `); }); }); @@ -834,58 +826,58 @@ describe('update()', () => { }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeParams": Object { + "bar": true, + }, + "enabled": true, + "id": "1", + "interval": "10s", + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1'); expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "interval": "10s", - } - `); + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeParams": Object { + "bar": true, + }, + "interval": "10s", + } + `); expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); }); it('should validate alertTypeParams', async () => { diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index c4f87270d5b7..cbd1ba3eab53 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -8,14 +8,14 @@ import { omit } from 'lodash'; import { SavedObjectsClientContract, SavedObjectReference } from 'src/core/server'; import { Alert, RawAlert, AlertTypeRegistry, AlertAction, Log } from './types'; import { TaskManager } from '../../task_manager'; -import { validateAlertTypeParams, parseDuration } from './lib'; +import { validateAlertTypeParams } from './lib'; interface ConstructorOptions { log: Log; taskManager: TaskManager; savedObjectsClient: SavedObjectsClientContract; alertTypeRegistry: AlertTypeRegistry; - basePath: string; + spaceId?: string; } interface FindOptions { @@ -34,6 +34,13 @@ interface FindOptions { }; } +interface FindResult { + page: number; + perPage: number; + total: number; + data: object[]; +} + interface CreateOptions { data: Alert; options?: { @@ -53,7 +60,7 @@ interface UpdateOptions { export class AlertsClient { private readonly log: Log; - private readonly basePath: string; + private readonly spaceId?: string; private readonly taskManager: TaskManager; private readonly savedObjectsClient: SavedObjectsClientContract; private readonly alertTypeRegistry: AlertTypeRegistry; @@ -63,10 +70,10 @@ export class AlertsClient { savedObjectsClient, taskManager, log, - basePath, + spaceId, }: ConstructorOptions) { this.log = log; - this.basePath = basePath; + this.spaceId = spaceId; this.taskManager = taskManager; this.alertTypeRegistry = alertTypeRegistry; this.savedObjectsClient = savedObjectsClient; @@ -90,8 +97,7 @@ export class AlertsClient { scheduledTask = await this.scheduleAlert( createdAlert.id, rawAlert.alertTypeId, - rawAlert.interval, - this.basePath + rawAlert.interval ); } catch (e) { // Cleanup data, something went wrong scheduling the task @@ -124,14 +130,22 @@ export class AlertsClient { return this.getAlertFromRaw(result.id, result.attributes, result.references); } - public async find({ options = {} }: FindOptions = {}) { + public async find({ options = {} }: FindOptions = {}): Promise { const results = await this.savedObjectsClient.find({ ...options, type: 'alert', }); - return results.saved_objects.map(result => + + const data = results.saved_objects.map(result => this.getAlertFromRaw(result.id, result.attributes, result.references) ); + + return { + page: results.page, + perPage: results.per_page, + total: results.total, + data, + }; } public async delete({ id }: { id: string }) { @@ -174,8 +188,7 @@ export class AlertsClient { const scheduledTask = await this.scheduleAlert( id, existingObject.attributes.alertTypeId, - existingObject.attributes.interval, - this.basePath + existingObject.attributes.interval ); await this.savedObjectsClient.update( 'alert', @@ -205,18 +218,15 @@ export class AlertsClient { } } - private async scheduleAlert(id: string, alertTypeId: string, interval: string, basePath: string) { + private async scheduleAlert(id: string, alertTypeId: string, interval: string) { return await this.taskManager.schedule({ taskType: `alerting:${alertTypeId}`, params: { alertId: id, - basePath, + spaceId: this.spaceId, }, state: { - // This is here because we can't rely on the task manager's internal runAt. - // It changes it for timeout, etc when a task is running. - scheduledRunAt: new Date(Date.now() + parseDuration(interval)), - previousScheduledRunAt: null, + previousStartedAt: null, alertTypeState: {}, alertInstances: {}, }, diff --git a/x-pack/legacy/plugins/alerting/server/init.ts b/x-pack/legacy/plugins/alerting/server/init.ts index ec1255cc5ac2..bf7b4b8009b9 100644 --- a/x-pack/legacy/plugins/alerting/server/init.ts +++ b/x-pack/legacy/plugins/alerting/server/init.ts @@ -18,8 +18,18 @@ import { import { AlertingPlugin, Services } from './types'; import { AlertTypeRegistry } from './alert_type_registry'; import { AlertsClient } from './alerts_client'; +import { SpacesPlugin } from '../../spaces'; +import { createOptionalPlugin } from '../../../server/lib/optional_plugin'; export function init(server: Legacy.Server) { + const config = server.config(); + const spaces = createOptionalPlugin( + config, + 'xpack.spaces', + server.plugins, + 'spaces' + ); + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); const savedObjectsRepositoryWithInternalUser = server.savedObjects.getSavedObjectsRepository( callWithInternalUser @@ -43,6 +53,14 @@ export function init(server: Legacy.Server) { taskManager: taskManager!, fireAction: server.plugins.actions!.fire, internalSavedObjectsRepository: savedObjectsRepositoryWithInternalUser, + getBasePath(...args) { + return spaces.isEnabled + ? spaces.getBasePath(...args) + : server.config().get('server.basePath'); + }, + spaceIdToNamespace(...args) { + return spaces.isEnabled ? spaces.spaceIdToNamespace(...args) : undefined; + }, }); // Register routes @@ -64,7 +82,7 @@ export function init(server: Legacy.Server) { savedObjectsClient, alertTypeRegistry, taskManager: taskManager!, - basePath: request.getBasePath(), + spaceId: spaces.isEnabled ? spaces.getSpaceId(request) : undefined, }); return alertsClient; }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.test.ts b/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.test.ts index ba8e35dbd7ba..bbafd60cdbc6 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.test.ts @@ -7,8 +7,10 @@ import { createFireHandler } from './create_fire_handler'; const createFireHandlerParams = { - basePath: '/s/default', fireAction: jest.fn(), + spaceId: 'default', + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + getBasePath: jest.fn().mockReturnValue(undefined), alertSavedObject: { id: '1', type: 'alert', @@ -47,18 +49,18 @@ test('calls fireAction per selected action', async () => { await fireHandler('default', {}, {}); expect(createFireHandlerParams.fireAction).toHaveBeenCalledTimes(1); expect(createFireHandlerParams.fireAction.mock.calls[0]).toMatchInlineSnapshot(` -Array [ - Object { - "basePath": "/s/default", - "id": "1", - "params": Object { - "contextVal": "My goes here", - "foo": true, - "stateVal": "My goes here", - }, - }, -] -`); + Array [ + Object { + "id": "1", + "params": Object { + "contextVal": "My goes here", + "foo": true, + "stateVal": "My goes here", + }, + "spaceId": "default", + }, + ] + `); }); test('limits fireAction per action group', async () => { @@ -72,18 +74,18 @@ test('context attribute gets parameterized', async () => { await fireHandler('default', { value: 'context-val' }, {}); expect(createFireHandlerParams.fireAction).toHaveBeenCalledTimes(1); expect(createFireHandlerParams.fireAction.mock.calls[0]).toMatchInlineSnapshot(` -Array [ - Object { - "basePath": "/s/default", - "id": "1", - "params": Object { - "contextVal": "My context-val goes here", - "foo": true, - "stateVal": "My goes here", - }, - }, -] -`); + Array [ + Object { + "id": "1", + "params": Object { + "contextVal": "My context-val goes here", + "foo": true, + "stateVal": "My goes here", + }, + "spaceId": "default", + }, + ] + `); }); test('state attribute gets parameterized', async () => { @@ -91,23 +93,23 @@ test('state attribute gets parameterized', async () => { await fireHandler('default', {}, { value: 'state-val' }); expect(createFireHandlerParams.fireAction).toHaveBeenCalledTimes(1); expect(createFireHandlerParams.fireAction.mock.calls[0]).toMatchInlineSnapshot(` -Array [ - Object { - "basePath": "/s/default", - "id": "1", - "params": Object { - "contextVal": "My goes here", - "foo": true, - "stateVal": "My state-val goes here", - }, - }, -] -`); + Array [ + Object { + "id": "1", + "params": Object { + "contextVal": "My goes here", + "foo": true, + "stateVal": "My state-val goes here", + }, + "spaceId": "default", + }, + ] + `); }); test('throws error if reference not found', async () => { const params = { - basePath: '/s/default', + spaceId: 'default', fireAction: jest.fn(), alertSavedObject: { id: '1', diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.ts b/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.ts index 3a271365105c..f51b374298a0 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/create_fire_handler.ts @@ -12,13 +12,13 @@ import { transformActionParams } from './transform_action_params'; interface CreateFireHandlerOptions { fireAction: ActionsPlugin['fire']; alertSavedObject: SavedObject; - basePath: string; + spaceId: string; } export function createFireHandler({ fireAction, alertSavedObject, - basePath, + spaceId, }: CreateFireHandlerOptions) { return async (actionGroup: string, context: Context, state: State) => { const alertActions: RawAlertAction[] = alertSavedObject.attributes.actions; @@ -43,7 +43,7 @@ export function createFireHandler({ await fireAction({ id: action.id, params: action.params, - basePath, + spaceId, }); } }; diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts index c4a992773e2e..95969d3d9a17 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts @@ -4,18 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ +import sinon from 'sinon'; import { schema } from '@kbn/config-schema'; import { AlertExecutorOptions } from '../types'; +import { ConcreteTaskInstance } from '../../../task_manager'; import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { getCreateTaskRunnerFunction } from './get_create_task_runner_function'; -const mockedNow = new Date('2019-06-03T18:55:25.982Z'); -const mockedLastRunAt = new Date('2019-06-03T18:55:20.982Z'); -(global as any).Date = class Date extends global.Date { - static now() { - return mockedNow.getTime(); - } -}; +let fakeTimer: sinon.SinonFakeTimers; +let mockedTaskInstance: ConcreteTaskInstance; + +beforeAll(() => { + fakeTimer = sinon.useFakeTimers(); + mockedTaskInstance = { + id: '', + attempts: 0, + status: 'running', + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: { + startedAt: new Date(Date.now() - 5 * 60 * 1000), + }, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + }; +}); + +afterAll(() => fakeTimer.restore()); const savedObjectsClient = SavedObjectsClientMock.create(); @@ -34,17 +54,8 @@ const getCreateTaskRunnerFunctionParams = { }, fireAction: jest.fn(), internalSavedObjectsRepository: savedObjectsClient, -}; - -const mockedTaskInstance = { - runAt: mockedLastRunAt, - state: { - scheduledRunAt: mockedLastRunAt, - }, - taskType: 'alerting:test', - params: { - alertId: '1', - }, + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + getBasePath: jest.fn().mockReturnValue(undefined), }; const mockedAlertTypeSavedObject = { @@ -84,24 +95,23 @@ test('successfully executes the task', async () => { const runner = createTaskRunner({ taskInstance: mockedTaskInstance }); const runnerResult = await runner.run(); expect(runnerResult).toMatchInlineSnapshot(` - Object { - "runAt": 2019-06-03T18:55:30.982Z, - "state": Object { - "alertInstances": Object {}, - "alertTypeState": undefined, - "previousScheduledRunAt": 2019-06-03T18:55:20.982Z, - "scheduledRunAt": 2019-06-03T18:55:30.982Z, - }, - } - `); + Object { + "runAt": 1970-01-01T00:00:10.000Z, + "state": Object { + "alertInstances": Object {}, + "alertTypeState": undefined, + "previousStartedAt": 1970-01-01T00:00:00.000Z, + }, + } + `); expect(getCreateTaskRunnerFunctionParams.alertType.executor).toHaveBeenCalledTimes(1); const call = getCreateTaskRunnerFunctionParams.alertType.executor.mock.calls[0][0]; expect(call.params).toMatchInlineSnapshot(` - Object { - "bar": true, - } - `); - expect(call.scheduledRunAt).toMatchInlineSnapshot(`2019-06-03T18:55:20.982Z`); + Object { + "bar": true, + } + `); + expect(call.startedAt).toMatchInlineSnapshot(`1970-01-01T00:00:00.000Z`); expect(call.state).toMatchInlineSnapshot(`Object {}`); expect(call.services.alertInstanceFactory).toBeTruthy(); expect(call.services.callCluster).toBeTruthy(); @@ -122,11 +132,11 @@ test('fireAction is called per alert instance that fired', async () => { expect(getCreateTaskRunnerFunctionParams.fireAction.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "basePath": undefined, "id": "1", "params": Object { "foo": true, }, + "spaceId": undefined, }, ] `); @@ -154,17 +164,17 @@ test('persists alertInstances passed in from state, only if they fire', async () }); const runnerResult = await runner.run(); expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object { - "lastFired": 1559588125982, - }, - "state": Object { - "bar": false, - }, - }, - } - `); + Object { + "1": Object { + "meta": Object { + "lastFired": 0, + }, + "state": Object { + "bar": false, + }, + }, + } + `); }); test('validates params before executing the alert type', async () => { diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts index c21ddbe7ed98..2ac3023a2079 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts @@ -7,22 +7,25 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { ActionsPlugin } from '../../../actions'; import { AlertType, Services, AlertServices } from '../types'; -import { TaskInstance } from '../../../task_manager'; +import { ConcreteTaskInstance } from '../../../task_manager'; import { createFireHandler } from './create_fire_handler'; import { createAlertInstanceFactory } from './create_alert_instance_factory'; import { AlertInstance } from './alert_instance'; import { getNextRunAt } from './get_next_run_at'; import { validateAlertTypeParams } from './validate_alert_type_params'; +import { SpacesPlugin } from '../../../spaces'; interface CreateTaskRunnerFunctionOptions { getServices: (basePath: string) => Services; alertType: AlertType; fireAction: ActionsPlugin['fire']; internalSavedObjectsRepository: SavedObjectsClientContract; + spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace']; + getBasePath: SpacesPlugin['getBasePath']; } interface TaskRunnerOptions { - taskInstance: TaskInstance; + taskInstance: ConcreteTaskInstance; } export function getCreateTaskRunnerFunction({ @@ -30,13 +33,17 @@ export function getCreateTaskRunnerFunction({ alertType, fireAction, internalSavedObjectsRepository, + spaceIdToNamespace, + getBasePath, }: CreateTaskRunnerFunctionOptions) { return ({ taskInstance }: TaskRunnerOptions) => { return { run: async () => { + const namespace = spaceIdToNamespace(taskInstance.params.spaceId); const alertSavedObject = await internalSavedObjectsRepository.get( 'alert', - taskInstance.params.alertId + taskInstance.params.alertId, + { namespace } ); // Validate @@ -48,7 +55,7 @@ export function getCreateTaskRunnerFunction({ const fireHandler = createFireHandler({ alertSavedObject, fireAction, - basePath: taskInstance.params.basePath, + spaceId: taskInstance.params.spaceId, }); const alertInstances: Record = {}; const alertInstancesData = taskInstance.state.alertInstances || {}; @@ -66,8 +73,8 @@ export function getCreateTaskRunnerFunction({ services: alertTypeServices, params: validatedAlertTypeParams, state: taskInstance.state.alertTypeState || {}, - scheduledRunAt: taskInstance.state.scheduledRunAt, - previousScheduledRunAt: taskInstance.state.previousScheduledRunAt, + startedAt: taskInstance.startedAt!, + previousStartedAt: taskInstance.state.previousStartedAt, }); await Promise.all( @@ -88,7 +95,7 @@ export function getCreateTaskRunnerFunction({ ); const nextRunAt = getNextRunAt( - new Date(taskInstance.state.scheduledRunAt), + new Date(taskInstance.startedAt!), alertSavedObject.attributes.interval ); @@ -96,9 +103,7 @@ export function getCreateTaskRunnerFunction({ state: { alertTypeState, alertInstances, - // We store nextRunAt ourselves since task manager changes runAt when executing a task - scheduledRunAt: nextRunAt, - previousScheduledRunAt: taskInstance.state.scheduledRunAt, + previousStartedAt: taskInstance.startedAt!, }, runAt: nextRunAt, }; diff --git a/x-pack/legacy/plugins/alerting/server/routes/delete.test.ts b/x-pack/legacy/plugins/alerting/server/routes/delete.test.ts index 80cddf4b56ff..7e3948640034 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/delete.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/delete.test.ts @@ -20,9 +20,8 @@ test('deletes an alert with proper parameters', async () => { alertsClient.delete.mockResolvedValueOnce({}); const { payload, statusCode } = await server.inject(request); - expect(statusCode).toBe(200); - const response = JSON.parse(payload); - expect(response).toEqual({}); + expect(statusCode).toBe(204); + expect(payload).toEqual(''); expect(alertsClient.delete).toHaveBeenCalledTimes(1); expect(alertsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/legacy/plugins/alerting/server/routes/delete.ts b/x-pack/legacy/plugins/alerting/server/routes/delete.ts index 9352bd372683..7dbb336b3cc1 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/delete.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/delete.ts @@ -26,10 +26,11 @@ export function deleteAlertRoute(server: Hapi.Server) { .required(), }, }, - async handler(request: DeleteRequest) { + async handler(request: DeleteRequest, h: Hapi.ResponseToolkit) { const { id } = request.params; const alertsClient = request.getAlertsClient!(); - return await alertsClient.delete({ id }); + await alertsClient.delete({ id }); + return h.response().code(204); }, }); } diff --git a/x-pack/legacy/plugins/alerting/server/routes/find.test.ts b/x-pack/legacy/plugins/alerting/server/routes/find.test.ts index 8bc41a51dc7a..73ab2ddd594f 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/find.test.ts @@ -26,11 +26,18 @@ test('sends proper arguments to alert find function', async () => { 'fields=description', }; - alertsClient.find.mockResolvedValueOnce([]); + const expectedResult = { + page: 1, + perPage: 1, + total: 0, + data: [], + }; + + alertsClient.find.mockResolvedValueOnce(expectedResult); const { payload, statusCode } = await server.inject(request); expect(statusCode).toBe(200); const response = JSON.parse(payload); - expect(response).toEqual([]); + expect(response).toEqual(expectedResult); expect(alertsClient.find).toHaveBeenCalledTimes(1); expect(alertsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 50fbba498226..b1e268431e40 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -29,8 +29,8 @@ export interface AlertServices extends Services { } export interface AlertExecutorOptions { - scheduledRunAt: Date; - previousScheduledRunAt?: Date; + startedAt: Date; + previousStartedAt?: Date; services: AlertServices; params: Record; state: State; diff --git a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 04b759ac007d..a7c61d1c79da 100644 --- a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -10,6 +10,8 @@ exports[`Error ERROR_GROUP_ID 1`] = `"grouping key"`; exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`; +exports[`Error ERROR_PAGE_URL 1`] = `undefined`; + exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Error METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; @@ -72,6 +74,8 @@ exports[`Error TRANSACTION_ID 1`] = `"transaction id"`; exports[`Error TRANSACTION_NAME 1`] = `undefined`; +exports[`Error TRANSACTION_PAGE_URL 1`] = `undefined`; + exports[`Error TRANSACTION_RESULT 1`] = `undefined`; exports[`Error TRANSACTION_SAMPLED 1`] = `undefined`; @@ -92,6 +96,8 @@ exports[`Span ERROR_GROUP_ID 1`] = `undefined`; exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`; +exports[`Span ERROR_PAGE_URL 1`] = `undefined`; + exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Span METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; @@ -154,6 +160,8 @@ exports[`Span TRANSACTION_ID 1`] = `"transaction id"`; exports[`Span TRANSACTION_NAME 1`] = `undefined`; +exports[`Span TRANSACTION_PAGE_URL 1`] = `undefined`; + exports[`Span TRANSACTION_RESULT 1`] = `undefined`; exports[`Span TRANSACTION_SAMPLED 1`] = `undefined`; @@ -174,6 +182,8 @@ exports[`Transaction ERROR_GROUP_ID 1`] = `undefined`; exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`; +exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`; + exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`; exports[`Transaction METRIC_JAVA_HEAP_MEMORY_COMMITTED 1`] = `undefined`; @@ -236,6 +246,8 @@ exports[`Transaction TRANSACTION_ID 1`] = `"transaction id"`; exports[`Transaction TRANSACTION_NAME 1`] = `"transaction name"`; +exports[`Transaction TRANSACTION_PAGE_URL 1`] = `undefined`; + exports[`Transaction TRANSACTION_RESULT 1`] = `"transaction result"`; exports[`Transaction TRANSACTION_SAMPLED 1`] = `true`; diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts index 1dbfa0592ec8..a5d057817893 100644 --- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts @@ -21,6 +21,7 @@ export const TRANSACTION_NAME = 'transaction.name'; export const TRANSACTION_ID = 'transaction.id'; export const TRANSACTION_SAMPLED = 'transaction.sampled'; export const TRANSACTION_BREAKDOWN_COUNT = 'transaction.breakdown.count'; +export const TRANSACTION_PAGE_URL = 'transaction.page.url'; export const TRACE_ID = 'trace.id'; @@ -40,6 +41,7 @@ export const ERROR_CULPRIT = 'error.culprit'; export const ERROR_LOG_MESSAGE = 'error.log.message'; export const ERROR_EXC_MESSAGE = 'error.exception.message'; // only to be used in es queries, since error.exception is now an array export const ERROR_EXC_HANDLED = 'error.exception.handled'; // only to be used in es queries, since error.exception is now an array +export const ERROR_PAGE_URL = 'error.page.url'; // METRICS export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free'; diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index aac946a36b45..bc12d90eea47 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -59,7 +59,8 @@ export const apm: LegacyPluginInitializer = kibana => { // display menu item ui: Joi.object({ enabled: Joi.boolean().default(true), - transactionGroupBucketSize: Joi.number().default(100) + transactionGroupBucketSize: Joi.number().default(100), + maxTraceItems: Joi.number().default(1000) }).default(), // enable plugin @@ -67,7 +68,7 @@ export const apm: LegacyPluginInitializer = kibana => { // buckets minimumBucketSize: Joi.number().default(15), - bucketTargetCount: Joi.number().default(27) + bucketTargetCount: Joi.number().default(15) }).default(); }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.test.tsx index 7fbc72bf5432..4d4991f161f6 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.test.tsx @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; import { APMError } from '../../../../../typings/es_schemas/ui/APMError'; import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; import { IStickyProperty } from '../../../shared/StickyProperties'; import { StickyErrorProperties } from './StickyErrorProperties'; +import { + ERROR_PAGE_URL, + URL_FULL +} from '../../../../../common/elasticsearch_fieldnames'; describe('StickyErrorProperties', () => { it('should render StickyProperties', () => { @@ -28,6 +32,7 @@ describe('StickyErrorProperties', () => { const error = { '@timestamp': 'myTimestamp', + agent: { name: 'nodejs' }, http: { request: { method: 'GET' } }, url: { full: 'myUrl' }, service: { name: 'myService' }, @@ -43,36 +48,70 @@ describe('StickyErrorProperties', () => { expect(wrapper).toMatchSnapshot(); }); - describe('error.exception.handled', () => { - function getIsHandledValue(error: APMError) { - const wrapper = shallow( - - ); + it('url.full', () => { + const error = { + agent: { name: 'nodejs' }, + url: { full: 'myFullUrl' } + } as APMError; - const stickyProps = wrapper.prop('stickyProperties') as IStickyProperty[]; - const handledProp = stickyProps.find( - prop => prop.fieldName === 'error.exception.handled' - ); + const wrapper = shallow( + + ); + const urlValue = getValueByFieldName(wrapper, URL_FULL); + expect(urlValue).toBe('myFullUrl'); + }); - return handledProp && handledProp.val; - } + it('error.page.url', () => { + const error = { + agent: { name: 'rum-js' }, + error: { page: { url: 'myPageUrl' } } + } as APMError; + const wrapper = shallow( + + ); + const urlValue = getValueByFieldName(wrapper, ERROR_PAGE_URL); + expect(urlValue).toBe('myPageUrl'); + }); + + describe('error.exception.handled', () => { it('should should render "true"', () => { - const error = { error: { exception: [{ handled: true }] } } as APMError; - const isHandledValue = getIsHandledValue(error); - expect(isHandledValue).toBe('true'); + const error = { + agent: { name: 'nodejs' }, + error: { exception: [{ handled: true }] } + } as APMError; + const wrapper = shallow( + + ); + const value = getValueByFieldName(wrapper, 'error.exception.handled'); + expect(value).toBe('true'); }); it('should should render "false"', () => { - const error = { error: { exception: [{ handled: false }] } } as APMError; - const isHandledValue = getIsHandledValue(error); - expect(isHandledValue).toBe('false'); + const error = { + agent: { name: 'nodejs' }, + error: { exception: [{ handled: false }] } + } as APMError; + const wrapper = shallow( + + ); + const value = getValueByFieldName(wrapper, 'error.exception.handled'); + expect(value).toBe('false'); }); it('should should render "N/A"', () => { - const error = {} as APMError; - const isHandledValue = getIsHandledValue(error); - expect(isHandledValue).toBe('N/A'); + const error = { agent: { name: 'nodejs' } } as APMError; + const wrapper = shallow( + + ); + const value = getValueByFieldName(wrapper, 'error.exception.handled'); + expect(value).toBe('N/A'); }); }); }); + +function getValueByFieldName(wrapper: ShallowWrapper, fieldName: string) { + const stickyProps = wrapper.prop('stickyProperties') as IStickyProperty[]; + const prop = stickyProps.find(p => p.fieldName === fieldName); + return prop && prop.val; +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.tsx index 567ad19e13c0..bdf122ca52c4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/StickyErrorProperties.tsx @@ -13,21 +13,22 @@ import { HTTP_REQUEST_METHOD, TRANSACTION_ID, URL_FULL, - USER_ID + USER_ID, + ERROR_PAGE_URL } from '../../../../../common/elasticsearch_fieldnames'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { APMError } from '../../../../../typings/es_schemas/ui/APMError'; import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; -import { APMLink } from '../../../shared/Links/APMLink'; -import { legacyEncodeURIComponent } from '../../../shared/Links/url_helpers'; import { StickyProperties } from '../../../shared/StickyProperties'; +import { TransactionLink } from '../../../shared/Links/apm/TransactionLink'; +import { isRumAgentName } from '../../../../../common/agent_name'; interface Props { error: APMError; transaction: Transaction | undefined; } -function TransactionLink({ +function TransactionLinkWrapper({ transaction }: { transaction: Transaction | undefined; @@ -41,27 +42,27 @@ function TransactionLink({ return {transaction.transaction.id}; } - const path = `/${ - transaction.service.name - }/transactions/${legacyEncodeURIComponent( - transaction.transaction.type - )}/${legacyEncodeURIComponent(transaction.transaction.name)}`; - return ( - + {transaction.transaction.id} - + ); } export function StickyErrorProperties({ error, transaction }: Props) { const isHandled = idx(error, _ => _.error.exception[0].handled); + const isRumAgent = isRumAgentName(error.agent.name); + + const { urlFieldName, urlValue } = isRumAgent + ? { + urlFieldName: ERROR_PAGE_URL, + urlValue: idx(error, _ => _.error.page.url) + } + : { + urlFieldName: URL_FULL, + urlValue: idx(error, _ => _.url.full) + }; + const stickyProperties = [ { fieldName: '@timestamp', @@ -72,12 +73,9 @@ export function StickyErrorProperties({ error, transaction }: Props) { width: '50%' }, { - fieldName: URL_FULL, + fieldName: urlFieldName, label: 'URL', - val: - idx(error, _ => _.context.page.url) || - idx(error, _ => _.url.full) || - NOT_AVAILABLE_LABEL, + val: urlValue || NOT_AVAILABLE_LABEL, truncated: true, width: '50%' }, @@ -105,7 +103,7 @@ export function StickyErrorProperties({ error, transaction }: Props) { defaultMessage: 'Transaction sample ID' } ), - val: , + val: , width: '25%' }, { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/StickyErrorProperties.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/StickyErrorProperties.test.tsx.snap index 257eb47c9564..74d0880d40fb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/StickyErrorProperties.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/StickyErrorProperties.test.tsx.snap @@ -32,7 +32,7 @@ exports[`StickyErrorProperties should render StickyProperties 1`] = ` Object { "fieldName": "transaction.id", "label": "Transaction sample ID", - "val": List should render with data 1`] = ` > a0ce2 @@ -670,9 +670,8 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` onMouseOver={[Function]} > List should render with data 1`] = ` onMouseOver={[Function]} >
    List should render with data 1`] = ` > f3ac9 @@ -779,9 +777,8 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` onMouseOver={[Function]} > List should render with data 1`] = ` onMouseOver={[Function]} >
    List should render with data 1`] = ` > e9086 @@ -888,9 +884,8 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` onMouseOver={[Function]} > List should render with data 1`] = ` onMouseOver={[Function]} >
    List should render with data 1`] = ` > 8673d @@ -997,9 +991,8 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` onMouseOver={[Function]} > List should render with data 1`] = ` onMouseOver={[Function]} >
    = props => { width: px(unit * 6), render: (groupId: string) => { return ( - + {groupId.slice(0, 5) || NOT_AVAILABLE_LABEL} ); @@ -85,7 +85,9 @@ const ErrorGroupList: React.FC = props => { id="error-message-tooltip" content={message || NOT_AVAILABLE_LABEL} > - + {message || NOT_AVAILABLE_LABEL} diff --git a/x-pack/legacy/plugins/apm/public/components/app/GlobalHelpExtension/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/GlobalHelpExtension/index.tsx index def6608eaf7f..fb10a65d975b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/GlobalHelpExtension/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/GlobalHelpExtension/index.tsx @@ -8,15 +8,17 @@ import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; import styled from 'styled-components'; -import chrome from 'ui/chrome'; import url from 'url'; import { px, units } from '../../../style/variables'; +import { useCore } from '../../../hooks/useCore'; const Container = styled.div` margin: ${px(units.minus)} 0; `; export const GlobalHelpExtension: React.SFC = () => { + const core = useCore(); + return ( @@ -33,7 +35,7 @@ export const GlobalHelpExtension: React.SFC = () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx index 665bcf7e1aac..404256085963 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx @@ -17,7 +17,7 @@ import { HistoryTabs, IHistoryTab } from '../../shared/HistoryTabs'; import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; import { ServiceOverview } from '../ServiceOverview'; import { TraceOverview } from '../TraceOverview'; -import { APMLink } from '../../shared/Links/APMLink'; +import { APMLink } from '../../shared/Links/apm/APMLink'; const homeTabs: IHistoryTab[] = [ { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx index 30aeb5fe3173..5eb2626f4487 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx @@ -7,14 +7,16 @@ import { Location } from 'history'; import { last } from 'lodash'; import React from 'react'; -import chrome from 'ui/chrome'; -import { getAPMHref } from '../../shared/Links/APMLink'; +import { InternalCoreStart } from 'src/core/public'; +import { useCore } from '../../../hooks/useCore'; +import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { Breadcrumb, ProvideBreadcrumbs } from './ProvideBreadcrumbs'; import { routes } from './route_config'; interface Props { location: Location; breadcrumbs: Breadcrumb[]; + core: InternalCoreStart; } class UpdateBreadcrumbsComponent extends React.Component { @@ -26,7 +28,7 @@ class UpdateBreadcrumbsComponent extends React.Component { const current = last(breadcrumbs) || { text: '' }; document.title = current.text; - chrome.breadcrumbs.set(breadcrumbs); + this.props.core.chrome.setBreadcrumbs(breadcrumbs); } public componentDidMount() { @@ -43,6 +45,7 @@ class UpdateBreadcrumbsComponent extends React.Component { } export function UpdateBreadcrumbs() { + const core = useCore(); return ( )} /> diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js index dd4fbdbb2449..20d747d32afa 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js @@ -7,35 +7,18 @@ import { mount } from 'enzyme'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import chrome from 'ui/chrome'; import { UpdateBreadcrumbs } from '../UpdateBreadcrumbs'; +import * as hooks from '../../../../hooks/useCore'; jest.mock('ui/kfetch'); -jest.mock( - 'ui/chrome', - () => ({ - breadcrumbs: { - set: jest.fn() - }, - getBasePath: () => `/some/base/path`, - getUiSettingsClient: () => { - return { - get: key => { - switch (key) { - case 'timepicker:timeDefaults': - return { from: 'now-15m', to: 'now', mode: 'quick' }; - case 'timepicker:refreshIntervalDefaults': - return { pause: false, value: 0 }; - default: - throw new Error(`Unexpected config key: ${key}`); - } - } - }; - } - }), - { virtual: true } -); +const coreMock = { + chrome: { + setBreadcrumbs: jest.fn() + } +}; + +jest.spyOn(hooks, 'useCore').mockReturnValue(coreMock); function expectBreadcrumbToMatchSnapshot(route) { mount( @@ -43,8 +26,8 @@ function expectBreadcrumbToMatchSnapshot(route) { ); - expect(chrome.breadcrumbs.set).toHaveBeenCalledTimes(1); - expect(chrome.breadcrumbs.set.mock.calls[0][0]).toMatchSnapshot(); + expect(coreMock.chrome.setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(coreMock.chrome.setBreadcrumbs.mock.calls[0][0]).toMatchSnapshot(); } describe('Breadcrumbs', () => { @@ -55,7 +38,7 @@ describe('Breadcrumbs', () => { global.document = { title: 'Kibana' }; - chrome.breadcrumbs.set.mockReset(); + coreMock.chrome.setBreadcrumbs.mockReset(); }); afterEach(() => { @@ -67,37 +50,37 @@ describe('Breadcrumbs', () => { expect(global.document.title).toMatchInlineSnapshot(`"APM"`); }); - it('/:serviceName/errors/:groupId', () => { - expectBreadcrumbToMatchSnapshot('/opbeans-node/errors/myGroupId'); + it('/services/:serviceName/errors/:groupId', () => { + expectBreadcrumbToMatchSnapshot('/services/opbeans-node/errors/myGroupId'); expect(global.document.title).toMatchInlineSnapshot(`"myGroupId"`); }); - it('/:serviceName/errors', () => { - expectBreadcrumbToMatchSnapshot('/opbeans-node/errors'); + it('/services/:serviceName/errors', () => { + expectBreadcrumbToMatchSnapshot('/services/opbeans-node/errors'); expect(global.document.title).toMatchInlineSnapshot(`"Errors"`); }); it('/:serviceName', () => { expectBreadcrumbToMatchSnapshot('/opbeans-node'); - expect(global.document.title).toMatchInlineSnapshot(`"opbeans-node"`); + expect(global.document.title).toMatchInlineSnapshot(`"APM"`); }); - it('/:serviceName/transactions', () => { - expectBreadcrumbToMatchSnapshot('/opbeans-node/transactions'); + it('/services/:serviceName/transactions', () => { + expectBreadcrumbToMatchSnapshot('/services/opbeans-node/transactions'); expect(global.document.title).toMatchInlineSnapshot(`"Transactions"`); }); - it('/:serviceName/transactions/:transactionType', () => { - expectBreadcrumbToMatchSnapshot('/opbeans-node/transactions/request'); + it('/services/:serviceName/transactions/:transactionType', () => { + expectBreadcrumbToMatchSnapshot( + '/services/opbeans-node/transactions/request' + ); expect(global.document.title).toMatchInlineSnapshot(`"Transactions"`); }); - it('/:serviceName/transactions/:transactionType/:transactionName', () => { + it('/services/:serviceName/transactions/:transactionType/:transactionName', () => { expectBreadcrumbToMatchSnapshot( - '/opbeans-node/transactions/request/my-transaction-name' - ); - expect(global.document.title).toMatchInlineSnapshot( - `"my-transaction-name"` + '/services/opbeans-node/transactions/request/my-transaction-name' ); + expect(global.document.title).toMatchInlineSnapshot(`"Transactions"`); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/__snapshots__/UpdateBreadcrumbs.test.js.snap b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/__snapshots__/UpdateBreadcrumbs.test.js.snap index 3274461c6552..d340659eef13 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/__snapshots__/UpdateBreadcrumbs.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/__snapshots__/UpdateBreadcrumbs.test.js.snap @@ -6,102 +6,114 @@ Array [ "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "APM", }, - Object { - "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "opbeans-node", - }, ] `; -exports[`Breadcrumbs /:serviceName/errors 1`] = ` +exports[`Breadcrumbs /services/:serviceName/errors 1`] = ` Array [ Object { "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "APM", }, Object { - "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": "#/services?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "text": "Services", + }, + Object { + "href": "#/services/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "opbeans-node", }, Object { - "href": "#/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": "#/services/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "Errors", }, ] `; -exports[`Breadcrumbs /:serviceName/errors/:groupId 1`] = ` +exports[`Breadcrumbs /services/:serviceName/errors/:groupId 1`] = ` Array [ Object { "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "APM", }, Object { - "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": "#/services?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "text": "Services", + }, + Object { + "href": "#/services/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "opbeans-node", }, Object { - "href": "#/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": "#/services/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "Errors", }, Object { - "href": "#/opbeans-node/errors/myGroupId?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": "#/services/opbeans-node/errors/myGroupId?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "myGroupId", }, ] `; -exports[`Breadcrumbs /:serviceName/transactions 1`] = ` +exports[`Breadcrumbs /services/:serviceName/transactions 1`] = ` Array [ Object { "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "APM", }, Object { - "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": "#/services?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "text": "Services", + }, + Object { + "href": "#/services/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "opbeans-node", }, Object { - "href": "#/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": "#/services/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "Transactions", }, ] `; -exports[`Breadcrumbs /:serviceName/transactions/:transactionType 1`] = ` +exports[`Breadcrumbs /services/:serviceName/transactions/:transactionType 1`] = ` Array [ Object { "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "APM", }, Object { - "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": "#/services?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "text": "Services", + }, + Object { + "href": "#/services/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "opbeans-node", }, Object { - "href": "#/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "href": "#/services/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "Transactions", }, ] `; -exports[`Breadcrumbs /:serviceName/transactions/:transactionType/:transactionName 1`] = ` +exports[`Breadcrumbs /services/:serviceName/transactions/:transactionType/:transactionName 1`] = ` Array [ Object { "href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", "text": "APM", }, Object { - "href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "opbeans-node", + "href": "#/services?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "text": "Services", }, Object { - "href": "#/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "Transactions", + "href": "#/services/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "text": "opbeans-node", }, Object { - "href": "#/opbeans-node/transactions/request/my-transaction-name?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", - "text": "my-transaction-name", + "href": "#/services/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery", + "text": "Transactions", }, ] `; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx index 5a2c16c9fbfe..9f19961d1361 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { legacyDecodeURIComponent } from '../../../shared/Links/url_helpers'; import { ErrorGroupDetails } from '../../ErrorGroupDetails'; import { ServiceDetails } from '../../ServiceDetails'; import { TransactionDetails } from '../../TransactionDetails'; @@ -15,6 +14,7 @@ import { Home } from '../../Home'; import { BreadcrumbRoute } from '../ProvideBreadcrumbs'; import { RouteName } from './route_names'; import { SettingsList } from '../../Settings/SettingsList'; +import { toQuery } from '../../../shared/Links/url_helpers'; interface RouteParams { serviceName: string; @@ -68,63 +68,62 @@ export const routes: BreadcrumbRoute[] = [ }, { exact: true, - path: '/:serviceName', + path: '/services/:serviceName', breadcrumb: ({ match }) => match.params.serviceName, render: (props: RouteComponentProps) => - renderAsRedirectTo(`/${props.match.params.serviceName}/transactions`)( - props - ), + renderAsRedirectTo( + `/services/${props.match.params.serviceName}/transactions` + )(props), name: RouteName.SERVICE }, + + // errors { exact: true, - path: '/:serviceName/errors/:groupId', + path: '/services/:serviceName/errors/:groupId', component: ErrorGroupDetails, breadcrumb: ({ match }) => match.params.groupId, name: RouteName.ERROR }, { exact: true, - path: '/:serviceName/errors', + path: '/services/:serviceName/errors', component: ServiceDetails, breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', { defaultMessage: 'Errors' }), name: RouteName.ERRORS }, + + // transactions { exact: true, - path: '/:serviceName/transactions', + path: '/services/:serviceName/transactions', component: ServiceDetails, breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', { defaultMessage: 'Transactions' }), name: RouteName.TRANSACTIONS }, - // Have to split this out as its own route to prevent duplicate breadcrumbs from both matching - // if we use :transactionType? as a single route here { exact: true, - path: '/:serviceName/transactions/:transactionType', - component: ServiceDetails, - breadcrumb: null, - name: RouteName.TRANSACTION_TYPE + path: '/services/:serviceName/transactions/view', + component: TransactionDetails, + breadcrumb: ({ location }) => { + const query = toQuery(location.search); + return query.transactionName as string; + }, + name: RouteName.TRANSACTION_NAME }, + + // metrics { exact: true, - path: '/:serviceName/metrics', + path: '/services/:serviceName/metrics', component: ServiceDetails, breadcrumb: i18n.translate('xpack.apm.breadcrumb.metricsTitle', { defaultMessage: 'Metrics' }), name: RouteName.METRICS - }, - { - exact: true, - path: '/:serviceName/transactions/:transactionType/:transactionName', - component: TransactionDetails, - breadcrumb: ({ match }) => - legacyDecodeURIComponent(match.params.transactionName) || '', - name: RouteName.TRANSACTION_NAME } ]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts b/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts index 47c95a5da5a7..866a5a6884d1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts @@ -7,12 +7,14 @@ import { i18n } from '@kbn/i18n'; import { useEffect } from 'react'; import { capabilities } from 'ui/capabilities'; -import chrome from 'ui/chrome'; +import { useCore } from '../../../hooks/useCore'; export const useUpdateBadgeEffect = () => { + const { chrome } = useCore(); + useEffect(() => { const uiCapabilities = capabilities.get(); - chrome.badge.set( + chrome.setBadge( !uiCapabilities.apm.save ? { text: i18n.translate('xpack.apm.header.badge.readOnly.text', { @@ -25,5 +27,5 @@ export const useUpdateBadgeEffect = () => { } : undefined ); - }, []); + }, [chrome]); }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index ec7d3a827ce5..4a5f4c53d6e4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -11,59 +11,55 @@ import { HistoryTabs } from '../../shared/HistoryTabs'; import { ErrorGroupOverview } from '../ErrorGroupOverview'; import { TransactionOverview } from '../TransactionOverview'; import { ServiceMetrics } from '../ServiceMetrics'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { loadServiceAgentName } from '../../../services/rest/apm/services'; +import { isRumAgentName } from '../../../../common/agent_name'; interface Props { - transactionTypes: string[]; urlParams: IUrlParams; - isRumAgent?: boolean; - agentName?: string; } -export function ServiceDetailTabs({ - transactionTypes, - urlParams, - isRumAgent, - agentName -}: Props) { - const { serviceName } = urlParams; - const headTransactionType = transactionTypes[0]; +export function ServiceDetailTabs({ urlParams }: Props) { + const { serviceName, start, end } = urlParams; + const { data: agentName } = useFetcher(() => { + if (serviceName && start && end) { + return loadServiceAgentName({ serviceName, start, end }); + } + }, [serviceName, start, end]); + const transactionsTab = { title: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', { defaultMessage: 'Transactions' }), - path: headTransactionType - ? `/${serviceName}/transactions/${headTransactionType}` - : `/${serviceName}/transactions`, - routePath: `/${serviceName}/transactions/:transactionType?`, - render: () => ( - - ), + path: `/services/${serviceName}/transactions`, + render: () => , name: 'transactions' }; + const errorsTab = { title: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', { defaultMessage: 'Errors' }), - path: `/${serviceName}/errors`, + path: `/services/${serviceName}/errors`, render: () => { return ; }, name: 'errors' }; - const metricsTab = { - title: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', { - defaultMessage: 'Metrics' - }), - path: `/${serviceName}/metrics`, - render: () => , - name: 'metrics' - }; - const tabs = isRumAgent - ? [transactionsTab, errorsTab] - : [transactionsTab, errorsTab, metricsTab]; + + const tabs = [transactionsTab, errorsTab]; + if (agentName && !isRumAgentName(agentName)) { + const metricsTab = { + title: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', { + defaultMessage: 'Metrics' + }), + path: `/services/${serviceName}/metrics`, + render: () => , + name: 'metrics' + }; + + tabs.push(metricsTab); + } return ; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx index aaad257ce4f0..cb2c1df7a709 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx @@ -17,7 +17,6 @@ interface Props { isOpen: boolean; onClose: () => void; urlParams: IUrlParams; - serviceTransactionTypes: string[]; } interface State { @@ -155,7 +154,7 @@ export class MachineLearningFlyout extends Component { }; public render() { - const { isOpen, onClose, urlParams, serviceTransactionTypes } = this.props; + const { isOpen, onClose, urlParams } = this.props; const { serviceName } = urlParams; const { isCreatingJob, hasIndexPattern } = this.state; @@ -169,8 +168,7 @@ export class MachineLearningFlyout extends Component { isCreatingJob={isCreatingJob} onClickCreate={this.onClickCreate} onClose={onClose} - serviceName={serviceName} - serviceTransactionTypes={serviceTransactionTypes} + urlParams={urlParams} /> ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx index 640214d4a708..92e63f0ada8f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx @@ -20,21 +20,23 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { isEmpty } from 'lodash'; import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher'; import { getHasMLJob } from '../../../../../services/rest/ml'; import { KibanaLink } from '../../../../shared/Links/KibanaLink'; import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; import { MLLink } from '../../../../shared/Links/MachineLearningLinks/MLLink'; import { TransactionSelect } from './TransactionSelect'; +import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; +import { useServiceTransactionTypes } from '../../../../../hooks/useServiceTransactionTypes'; interface Props { hasIndexPattern: boolean; isCreatingJob: boolean; onClickCreate: ({ transactionType }: { transactionType: string }) => void; onClose: () => void; - serviceName: string; - serviceTransactionTypes: string[]; + urlParams: IUrlParams; } export function MachineLearningFlyoutView({ @@ -42,16 +44,31 @@ export function MachineLearningFlyoutView({ isCreatingJob, onClickCreate, onClose, - serviceName, - serviceTransactionTypes + urlParams }: Props) { - const [transactionType, setTransactionType] = useState( - serviceTransactionTypes[0] - ); - const { data: hasMLJob = false, status } = useFetcher( - () => getHasMLJob({ serviceName, transactionType }), - [serviceName, transactionType] - ); + const { serviceName } = urlParams; + const transactionTypes = useServiceTransactionTypes(urlParams); + + const [selectedTransactionType, setSelectedTransactionType] = useState< + string | undefined + >(undefined); + const { data: hasMLJob = false, status } = useFetcher(() => { + if (serviceName && selectedTransactionType) { + return getHasMLJob({ + serviceName, + transactionType: selectedTransactionType + }); + } + }, [serviceName, selectedTransactionType]); + + // update selectedTransactionType when list of transaction types has loaded + useEffect(() => { + setSelectedTransactionType(transactionTypes[0]); + }, [transactionTypes]); + + if (!serviceName || !selectedTransactionType || isEmpty(transactionTypes)) { + return null; + } const isLoadingMLJob = status === FETCH_STATUS.LOADING; @@ -91,13 +108,13 @@ export function MachineLearningFlyoutView({ 'There is currently a job running for {serviceName} ({transactionType}).', values: { serviceName, - transactionType + transactionType: selectedTransactionType } } )}{' '} {i18n.translate( 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText', @@ -199,12 +216,12 @@ export function MachineLearningFlyoutView({ - {serviceTransactionTypes.length > 1 ? ( + {transactionTypes.length > 1 ? ( { - setTransactionType(value); + setSelectedTransactionType(value); }} /> ) : null} @@ -212,7 +229,9 @@ export function MachineLearningFlyoutView({ onClickCreate({ transactionType })} + onClick={() => + onClickCreate({ transactionType: selectedTransactionType }) + } fill disabled={ isCreatingJob || diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index 45701f414b01..134934ff8425 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -30,20 +30,20 @@ import { memoize, padLeft, range } from 'lodash'; import moment from 'moment-timezone'; import React, { Component } from 'react'; import styled from 'styled-components'; -import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; +import { InternalCoreStart } from 'src/core/public'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { KibanaLink } from '../../../shared/Links/KibanaLink'; import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch'; import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; +import { CoreContext } from '../../../../context/CoreContext'; type ScheduleKey = keyof Schedule; -const getUserTimezone = memoize(() => { - const uiSettings = chrome.getUiSettingsClient(); - return uiSettings.get('dateFormat:tz') === 'Browser' +const getUserTimezone = memoize((core: InternalCoreStart): string => { + return core.uiSettings.get('dateFormat:tz') === 'Browser' ? moment.tz.guess() - : uiSettings.get('dateFormat:tz'); + : core.uiSettings.get('dateFormat:tz'); }); const SmallInput = styled.div` @@ -83,6 +83,7 @@ export class WatcherFlyout extends Component< WatcherFlyoutProps, WatcherFlyoutState > { + static contextType = CoreContext; public state: WatcherFlyoutState = { schedule: 'daily', threshold: 10, @@ -155,6 +156,7 @@ export class WatcherFlyout extends Component< }; public createWatch = () => { + const core: InternalCoreStart = this.context; const { serviceName } = this.props.urlParams; if (!serviceName) { @@ -190,13 +192,18 @@ export class WatcherFlyout extends Component< unit: 'h' }; + const apmIndexPatternTitle = core.injectedMetadata.getInjectedVar( + 'apmIndexPatternTitle' + ) as string; + return createErrorGroupWatch({ emails, schedule, serviceName, slackUrl, threshold: this.state.threshold, - timeRange + timeRange, + apmIndexPatternTitle }) .then((id: string) => { this.props.onClose(); @@ -271,7 +278,8 @@ export class WatcherFlyout extends Component< return null; } - const userTimezoneSetting = getUserTimezone(); + const core: InternalCoreStart = this.context; + const userTimezoneSetting = getUserTimezone(core); const dailyTime = this.state.daily; const inputTime = `${dailyTime}Z`; // Add tz to make into UTC const inputFormat = 'HH:mmZ'; // Parse as 24 hour w. tz diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index c952ce7f7ced..1bc2c8e2a84e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -6,7 +6,6 @@ import { isArray, isObject, isString } from 'lodash'; import mustache from 'mustache'; -import chrome from 'ui/chrome'; import uuid from 'uuid'; import { StringMap } from '../../../../../../typings/common'; // @ts-ignore @@ -23,7 +22,6 @@ describe('createErrorGroupWatch', () => { let createWatchResponse: string; let tmpl: any; beforeEach(async () => { - chrome.getInjected = jest.fn().mockReturnValue('myIndexPattern'); jest.spyOn(uuid, 'v4').mockReturnValue(new Buffer('mocked-uuid')); jest.spyOn(rest, 'createWatch').mockReturnValue(undefined); @@ -37,7 +35,8 @@ describe('createErrorGroupWatch', () => { serviceName: 'opbeans-node', slackUrl: 'https://hooks.slack.com/services/slackid1/slackid2/slackid3', threshold: 10, - timeRange: { value: 24, unit: 'h' } + timeRange: { value: 24, unit: 'h' }, + apmIndexPatternTitle: 'myIndexPattern' }); const watchBody = rest.createWatch.mock.calls[0][1]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index 81ec61fc1bb5..2617fef6de1d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; -import chrome from 'ui/chrome'; import url from 'url'; import uuid from 'uuid'; import { @@ -46,6 +45,7 @@ interface Arguments { value: number; unit: string; }; + apmIndexPatternTitle: string; } interface Actions { @@ -60,10 +60,10 @@ export async function createErrorGroupWatch({ serviceName, slackUrl, threshold, - timeRange + timeRange, + apmIndexPatternTitle }: Arguments) { const id = `apm-${uuid.v4()}`; - const apmIndexPatternTitle = chrome.getInjected('apmIndexPatternTitle'); const slackUrlPath = getSlackPathUrl(slackUrl); const emailTemplate = i18n.translate( diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx index 9835c01fa101..513c58d7a834 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx @@ -13,14 +13,15 @@ import { import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; import React, { Fragment } from 'react'; -import chrome from 'ui/chrome'; +import { InternalCoreStart } from 'src/core/public'; +import { idx } from '@kbn/elastic-idx'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { LicenseContext } from '../../../../context/LicenseContext'; import { MachineLearningFlyout } from './MachineLearningFlyout'; import { WatcherFlyout } from './WatcherFlyout'; +import { CoreContext } from '../../../../context/CoreContext'; interface Props { - transactionTypes: string[]; urlParams: IUrlParams; } interface State { @@ -30,9 +31,10 @@ interface State { type FlyoutName = null | 'ML' | 'Watcher'; export class ServiceIntegrations extends React.Component { + static contextType = CoreContext; public state: State = { isPopoverOpen: false, activeFlyout: null }; - public getPanelItems = memoize((mlAvailable: boolean) => { + public getPanelItems = memoize((mlAvailable: boolean | undefined) => { let panelItems: EuiContextMenuPanelItemDescriptor[] = []; if (mlAvailable) { panelItems = panelItems.concat(this.getMLPanelItems()); @@ -65,6 +67,8 @@ export class ServiceIntegrations extends React.Component { }; public getWatcherPanelItems = () => { + const core: InternalCoreStart = this.context; + return [ { name: i18n.translate( @@ -87,7 +91,7 @@ export class ServiceIntegrations extends React.Component { } ), icon: 'watchesApp', - href: chrome.addBasePath( + href: core.http.basePath.prepend( '/app/kibana#/management/elasticsearch/watcher' ), target: '_blank', @@ -144,7 +148,9 @@ export class ServiceIntegrations extends React.Component { panels={[ { id: 0, - items: this.getPanelItems(license.features.ml.is_available) + items: this.getPanelItems( + idx(license, _ => _.features.ml.is_available) + ) } ]} /> @@ -153,7 +159,6 @@ export class ServiceIntegrations extends React.Component { isOpen={this.state.activeFlyout === 'ML'} onClose={this.closeFlyouts} urlParams={this.props.urlParams} - serviceTransactionTypes={this.props.transactionTypes} /> { - if (serviceName && start && end) { - return loadServiceDetails({ serviceName, start, end, uiFilters }); - } - }, [serviceName, start, end, uiFilters]); - - if (!serviceDetailsData) { - return null; - } - - const isRumAgent = isRumAgentName(serviceDetailsData.agentName); + const { urlParams } = useUrlParams(); + const { serviceName } = urlParams; return (
    @@ -39,20 +25,12 @@ export function ServiceDetails() { - + - +
    ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 9d28db93d6f0..5e76140ce21e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -12,7 +12,7 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; interface ServiceMetricsProps { - agentName?: string; + agentName: string; } export function ServiceMetrics({ agentName }: ServiceMetricsProps) { @@ -21,18 +21,18 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) { const { start, end } = urlParams; return ( - - {data.charts.map(chart => ( - - - + + + {data.charts.map(chart => ( + + - - - - ))} - - + +
    + ))} + + + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx index 0b8759057634..de058d6ef973 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx @@ -10,18 +10,24 @@ import React from 'react'; import { KibanaLink } from '../../shared/Links/KibanaLink'; import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { ErrorStatePrompt } from '../../shared/ErrorStatePrompt'; interface Props { // any data submitted from APM agents found (not just in the given time range) historicalDataFound: boolean; - isLoading: boolean; + status: FETCH_STATUS | undefined; } -export function NoServicesMessage({ historicalDataFound, isLoading }: Props) { - if (isLoading) { +export function NoServicesMessage({ historicalDataFound, status }: Props) { + if (status === 'loading') { return ; } + if (status === 'failure') { + return ; + } + if (historicalDataFound) { return ( List should render columns correctly 1`] = ` position="top" > opbeans-python diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 8ae355aa53cf..31e05379928e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -12,7 +12,7 @@ import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_s import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { fontSizes, truncate } from '../../../../style/variables'; import { asDecimal, asMillis } from '../../../../utils/formatters'; -import { APMLink } from '../../../shared/Links/APMLink'; +import { APMLink } from '../../../shared/Links/apm/APMLink'; import { ManagedTable } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; @@ -50,7 +50,7 @@ export const SERVICE_COLUMNS = [ sortable: true, render: (serviceName: string) => ( - + {formatString(serviceName)} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx index 8b68e6207bb4..fdf373bf97f9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx @@ -7,19 +7,20 @@ import { shallow } from 'enzyme'; import React from 'react'; import { NoServicesMessage } from '../NoServicesMessage'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; describe('NoServicesMessage', () => { - it('should show only a "not found" message when historical data is found', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - it('should show a "no services installed" message, a link to the set up instructions page, a message about upgrading APM server, and a link to the upgrade assistant when NO historical data is found', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); + Object.values(FETCH_STATUS).forEach(status => { + [true, false].forEach(historicalDataFound => { + it(`status: ${status} and historicalDataFound: ${historicalDataFound}`, () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + }); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index b616111d7b71..013d803e6411 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -10,7 +10,9 @@ import 'react-testing-library/cleanup-after-each'; import { toastNotifications } from 'ui/notify'; import * as apmRestServices from '../../../../services/rest/apm/services'; import { ServiceOverview } from '..'; -import * as hooks from '../../../../hooks/useUrlParams'; +import * as urlParamsHooks from '../../../../hooks/useUrlParams'; +import * as coreHooks from '../../../../hooks/useCore'; +import { InternalCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); @@ -20,13 +22,22 @@ function renderServiceOverview() { describe('Service Overview -> View', () => { beforeEach(() => { + const coreMock = ({ + http: { + basePath: { + prepend: (path: string) => `/basepath${path}` + } + } + } as unknown) as InternalCoreStart; + // mock urlParams - spyOn(hooks, 'useUrlParams').and.returnValue({ + spyOn(urlParamsHooks, 'useUrlParams').and.returnValue({ urlParams: { start: 'myStart', end: 'myEnd' } }); + spyOn(coreHooks, 'useCore').and.returnValue(coreMock); }); afterEach(() => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap index 02dbe22aeb19..42e2b59da2e1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap @@ -1,6 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`NoServicesMessage should show a "no services installed" message, a link to the set up instructions page, a message about upgrading APM server, and a link to the upgrade assistant when NO historical data is found 1`] = ` +exports[`NoServicesMessage status: failure and historicalDataFound: false 1`] = ``; + +exports[`NoServicesMessage status: failure and historicalDataFound: true 1`] = ``; + +exports[`NoServicesMessage status: loading and historicalDataFound: false 1`] = ``; + +exports[`NoServicesMessage status: loading and historicalDataFound: true 1`] = ``; + +exports[`NoServicesMessage status: success and historicalDataFound: false 1`] = ` `; -exports[`NoServicesMessage should show only a "not found" message when historical data is found 1`] = ` +exports[`NoServicesMessage status: success and historicalDataFound: true 1`] = ` Learn more by visiting the Kibana Upgrade Assistant @@ -96,7 +96,7 @@ NodeList [ />
    ); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx index 7febbcd63661..d9e024c5560e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx @@ -18,7 +18,7 @@ import { Location } from 'history'; import React from 'react'; import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/Transaction'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { TransactionLink } from '../../../shared/Links/TransactionLink'; +import { TransactionLink } from '../../../shared/Links/apm/TransactionLink'; import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; import { StickyTransactionProperties } from './StickyTransactionProperties'; import { TransactionTabs } from './TransactionTabs'; @@ -97,13 +97,15 @@ interface Props { urlParams: IUrlParams; location: Location; waterfall: IWaterfall; + exceedsMax: boolean; } export const Transaction: React.SFC = ({ transaction, urlParams, location, - waterfall + waterfall, + exceedsMax }) => { return ( @@ -149,6 +151,7 @@ export const Transaction: React.SFC = ({ location={location} urlParams={urlParams} waterfall={waterfall} + exceedsMax={exceedsMax} /> ); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx index e08285ae9b96..f242690beab0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -31,7 +31,7 @@ export function TransactionDetails() { const { data: transactionChartsData } = useTransactionCharts(); - const { data: waterfall } = useWaterfall(urlParams); + const { data: waterfall, exceedsMax } = useWaterfall(urlParams); const transaction = waterfall.getTransactionById(urlParams.transactionId); const { transactionName } = urlParams; @@ -81,6 +81,7 @@ export function TransactionDetails() { transaction={transaction} urlParams={urlParams} waterfall={waterfall} + exceedsMax={exceedsMax} /> )}
    diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx index 0f9199be0894..3f6cca5dcb61 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx @@ -13,24 +13,22 @@ import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/ import { fontFamilyCode, truncate } from '../../../../style/variables'; import { asDecimal, asMillis } from '../../../../utils/formatters'; import { ImpactBar } from '../../../shared/ImpactBar'; -import { APMLink } from '../../../shared/Links/APMLink'; -import { legacyEncodeURIComponent } from '../../../shared/Links/url_helpers'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { EmptyMessage } from '../../../shared/EmptyMessage'; +import { TransactionLink } from '../../../shared/Links/apm/TransactionLink'; -const TransactionNameLink = styled(APMLink)` +const TransactionNameLink = styled(TransactionLink)` ${truncate('100%')}; font-family: ${fontFamilyCode}; `; interface Props { items: ITransactionGroup[]; - serviceName: string; isLoading: boolean; } -export function TransactionList({ items, serviceName, isLoading }: Props) { +export function TransactionList({ items, isLoading }: Props) { const columns: Array> = useMemo( () => [ { @@ -40,19 +38,13 @@ export function TransactionList({ items, serviceName, isLoading }: Props) { }), width: '50%', sortable: true, - render: (transactionName: string, data: typeof items[0]) => { - const encodedType = legacyEncodeURIComponent( - data.sample.transaction.type - ); - const encodedName = legacyEncodeURIComponent(transactionName); - const transactionPath = `/${serviceName}/transactions/${encodedType}/${encodedName}`; - + render: (transactionName: string, item: ITransactionGroup) => { return ( - + {transactionName || NOT_AVAILABLE_LABEL} @@ -111,7 +103,7 @@ export function TransactionList({ items, serviceName, isLoading }: Props) { render: (value: number) => } ], - [serviceName] + [] ); const noItemsMessage = ( diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx index fd70356d3553..7b1770dd4745 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx @@ -7,8 +7,10 @@ import React from 'react'; import { queryByLabelText, render } from 'react-testing-library'; import { TransactionOverview } from '..'; -import * as hooks from '../../../../hooks/useLocation'; +import * as useLocationHook from '../../../../hooks/useLocation'; import { history } from '../../../../utils/history'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import * as useServiceTransactionTypesHook from '../../../../hooks/useServiceTransactionTypes'; jest.mock('ui/kfetch'); @@ -25,18 +27,20 @@ afterAll(() => { function setup({ urlParams, serviceTransactionTypes -}: Parameters[0]) { +}: { + urlParams: IUrlParams; + serviceTransactionTypes: string[]; +}) { jest.spyOn(history, 'replace'); - jest.spyOn(hooks, 'useLocation').mockReturnValue({ pathname: '' } as any); - jest.spyOn(hooks, 'useLocation').mockReturnValue({ pathname: '' } as any); + jest + .spyOn(useLocationHook, 'useLocation') + .mockReturnValue({ pathname: '' } as any); - const { container } = render( - - ); + jest + .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypes') + .mockReturnValue(serviceTransactionTypes); + const { container } = render(); return { container }; } @@ -61,7 +65,7 @@ describe('TransactionOverview', () => { }); expect(history.replace).toHaveBeenCalledWith( expect.objectContaining({ - pathname: '/MyServiceName/transactions/firstType' + search: 'transactionType=firstType' }) ); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx index 3c5b504938f3..e2244e7ea108 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -20,7 +20,6 @@ import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; import { TransactionCharts } from '../../shared/charts/TransactionCharts'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; -import { legacyEncodeURIComponent } from '../../shared/Links/url_helpers'; import { TransactionList } from './List'; import { useRedirect } from './useRedirect'; import { useFetcher } from '../../../hooks/useFetcher'; @@ -29,10 +28,11 @@ import { history } from '../../../utils/history'; import { useLocation } from '../../../hooks/useLocation'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../infra/public'; +import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; +import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; interface Props { urlParams: IUrlParams; - serviceTransactionTypes: string[]; } function getRedirectLocation({ @@ -43,24 +43,28 @@ function getRedirectLocation({ location: Location; urlParams: IUrlParams; serviceTransactionTypes: string[]; -}) { - const { serviceName, transactionType } = urlParams; +}): Location | undefined { + const { transactionType } = urlParams; const firstTransactionType = first(serviceTransactionTypes); + if (!transactionType && firstTransactionType) { return { ...location, - pathname: `/${serviceName}/transactions/${firstTransactionType}` + search: fromQuery({ + ...toQuery(location.search), + transactionType: firstTransactionType + }) }; } } -export function TransactionOverview({ - urlParams, - serviceTransactionTypes -}: Props) { +export function TransactionOverview({ urlParams }: Props) { const location = useLocation(); const { serviceName, transactionType } = urlParams; + // TODO: fetching of transaction types should perhaps be lifted since it is needed in several places. Context? + const serviceTransactionTypes = useServiceTransactionTypes(urlParams); + // redirect to first transaction type useRedirect( history, @@ -75,6 +79,16 @@ export function TransactionOverview({ useTrackPageview({ app: 'apm', path: 'transaction_overview' }); useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 }); + const { + data: transactionListData, + status: transactionListStatus + } = useTransactionList(urlParams); + + const { data: hasMLJob = false } = useFetcher(() => { + if (serviceName && transactionType) { + return getHasMLJob({ serviceName, transactionType }); + } + }, [serviceName, transactionType]); // TODO: improve urlParams typings. // `serviceName` or `transactionType` will never be undefined here, and this check should not be needed @@ -82,17 +96,9 @@ export function TransactionOverview({ return null; } - const { - data: transactionListData, - status: transactionListStatus - } = useTransactionList(urlParams); - const { data: hasMLJob = false } = useFetcher( - () => getHasMLJob({ serviceName, transactionType }), - [serviceName, transactionType] - ); - return ( + {/* TODO: This should be replaced by local filters */} {serviceTransactionTypes.length > 1 ? ( { - const type = legacyEncodeURIComponent(event.target.value); history.push({ ...location, - pathname: `/${urlParams.serviceName}/transactions/${type}` + pathname: `/services/${urlParams.serviceName}/transactions`, + search: fromQuery({ + ...toQuery(location.search), + transactionType: event.target.value + }) }); }} /> @@ -143,7 +152,6 @@ export function TransactionOverview({ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ErrorStatePrompt.tsx b/x-pack/legacy/plugins/apm/public/components/shared/ErrorStatePrompt.tsx new file mode 100644 index 000000000000..7154d006fcaa --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/ErrorStatePrompt.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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +export function ErrorStatePrompt() { + return ( + + {i18n.translate('xpack.apm.error.prompt.title', { + defaultMessage: `Sorry, an error occured :(` + })} +
    + } + body={i18n.translate('xpack.apm.error.prompt.body', { + defaultMessage: `Please inspect your browser's developer console for details.` + })} + titleSize="s" + /> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 0bef2e49a77e..cc885fabb74d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,21 +7,19 @@ import React, { useState, useEffect } from 'react'; import { uniqueId, startsWith } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; -import chrome from 'ui/chrome'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AutocompleteSuggestion } from 'ui/autocomplete_providers'; -import { StaticIndexPattern } from 'ui/index_patterns'; +import { + AutocompleteSuggestion, + getAutocompleteProvider +} from 'ui/autocomplete_providers'; +import { StaticIndexPattern, getFromSavedObject } from 'ui/index_patterns'; +import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { fromQuery, toQuery } from '../Links/url_helpers'; import { KibanaLink } from '../Links/KibanaLink'; // @ts-ignore import { Typeahead } from './Typeahead'; -import { - convertKueryToEsQuery, - getSuggestions, - getAPMIndexPatternForKuery -} from '../../../services/kuery'; // @ts-ignore import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; @@ -29,6 +27,8 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; import { useMatchedRoutes } from '../../../hooks/useMatchedRoutes'; import { RouteName } from '../../app/Main/route_config/route_names'; +import { useCore } from '../../../hooks/useCore'; +import { getAPMIndexPattern } from '../../../services/rest/savedObjects'; const Container = styled.div` margin-bottom: 10px; @@ -41,7 +41,52 @@ interface State { isLoadingSuggestions: boolean; } +function convertKueryToEsQuery( + kuery: string, + indexPattern: StaticIndexPattern +) { + const ast = fromKueryExpression(kuery); + return toElasticsearchQuery(ast, indexPattern); +} + +async function getAPMIndexPatternForKuery(): Promise< + StaticIndexPattern | undefined +> { + const apmIndexPattern = await getAPMIndexPattern(); + if (!apmIndexPattern) { + return; + } + return getFromSavedObject(apmIndexPattern); +} + +function getSuggestions( + query: string, + selectionStart: number, + apmIndexPattern: StaticIndexPattern, + boolFilter: unknown +) { + const autocompleteProvider = getAutocompleteProvider('kuery'); + if (!autocompleteProvider) { + return []; + } + const config = { + get: () => true + }; + + const getAutocompleteSuggestions = autocompleteProvider({ + config, + indexPatterns: [apmIndexPattern], + boolFilter + }); + return getAutocompleteSuggestions({ + query, + selectionStart, + selectionEnd: selectionStart + }); +} + export function KueryBar() { + const core = useCore(); const [state, setState] = useState({ indexPattern: null, suggestions: [], @@ -52,7 +97,9 @@ export function KueryBar() { const location = useLocation(); const matchedRoutes = useMatchedRoutes(); - const apmIndexPatternTitle = chrome.getInjected('apmIndexPatternTitle'); + const apmIndexPatternTitle = core.injectedMetadata.getInjectedVar( + 'apmIndexPatternTitle' + ); const indexPatternMissing = !state.isLoadingIndexPattern && !state.indexPattern; let currentRequestCheck; @@ -72,15 +119,19 @@ export function KueryBar() { let didCancel = false; async function loadIndexPattern() { - setState({ ...state, isLoadingIndexPattern: true }); + setState(value => ({ ...value, isLoadingIndexPattern: true })); const indexPattern = await getAPMIndexPatternForKuery(); if (didCancel) { return; } if (!indexPattern) { - setState({ ...state, isLoadingIndexPattern: false }); + setState(value => ({ ...value, isLoadingIndexPattern: false })); } else { - setState({ ...state, indexPattern, isLoadingIndexPattern: false }); + setState(value => ({ + ...value, + indexPattern, + isLoadingIndexPattern: false + })); } } loadIndexPattern(); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index d5633611f09e..0673ab5e75cc 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -6,12 +6,12 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import chrome from 'ui/chrome'; import url from 'url'; import rison, { RisonValue } from 'rison-node'; import { useAPMIndexPattern } from '../../../../hooks/useAPMIndexPattern'; import { useLocation } from '../../../../hooks/useLocation'; import { getTimepickerRisonData } from '../rison_helpers'; +import { useCore } from '../../../../hooks/useCore'; interface Props { query: { @@ -31,6 +31,7 @@ interface Props { } export function DiscoverLink({ query = {}, ...rest }: Props) { + const core = useCore(); const apmIndexPattern = useAPMIndexPattern(); const location = useLocation(); @@ -47,7 +48,7 @@ export function DiscoverLink({ query = {}, ...rest }: Props) { }; const href = url.format({ - pathname: chrome.addBasePath('/app/kibana'), + pathname: core.http.basePath.prepend('/app/kibana'), hash: `/discover?_g=${rison.encode(risonQuery._g)}&_a=${rison.encode( risonQuery._a as RisonValue )}` diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index 91e055411cc0..a47df4886c40 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -14,6 +14,8 @@ import { getRenderedHref } from '../../../../../utils/testHelpers'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; import { DiscoverSpanLink } from '../DiscoverSpanLink'; import { DiscoverTransactionLink } from '../DiscoverTransactionLink'; +import * as hooks from '../../../../../hooks/useCore'; +import { InternalCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); @@ -25,6 +27,16 @@ jest beforeAll(() => { jest.spyOn(console, 'error').mockImplementation(() => null); + + const coreMock = ({ + http: { + basePath: { + prepend: (path: string) => `/basepath${path}` + } + } + } as unknown) as InternalCoreStart; + + jest.spyOn(hooks, 'useCore').mockReturnValue(coreMock); }); afterAll(() => { @@ -49,7 +61,7 @@ test('DiscoverTransactionLink should produce the correct URL', async () => { ); expect(href).toEqual( - `/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'processor.event:"transaction" AND transaction.id:"8b60bd32ecc6e150" AND trace.id:"8b60bd32ecc6e1506735a8b6cfcf175c"'))` + `/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'processor.event:"transaction" AND transaction.id:"8b60bd32ecc6e150" AND trace.id:"8b60bd32ecc6e1506735a8b6cfcf175c"'))` ); }); @@ -65,7 +77,7 @@ test('DiscoverSpanLink should produce the correct URL', async () => { } as Location); expect(href).toEqual( - `/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'span.id:"test-span-id"'))` + `/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'span.id:"test-span-id"'))` ); }); @@ -86,7 +98,7 @@ test('DiscoverErrorLink should produce the correct URL', async () => { ); expect(href).toEqual( - `/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'service.name:"service-name" AND error.grouping_key:"grouping-key"'),sort:('@timestamp':desc))` + `/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'service.name:"service-name" AND error.grouping_key:"grouping-key"'),sort:('@timestamp':desc))` ); }); @@ -108,6 +120,6 @@ test('DiscoverErrorLink should include optional kuery string in URL', async () = ); expect(href).toEqual( - `/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'service.name:"service-name" AND error.grouping_key:"grouping-key" AND some:kuery-string'),sort:('@timestamp':desc))` + `/basepath/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'service.name:"service-name" AND error.grouping_key:"grouping-key" AND some:kuery-string'),sort:('@timestamp':desc))` ); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx index 832cb13f3ba2..9925d87a159c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx @@ -8,11 +8,18 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../utils/testHelpers'; import { InfraLink } from './InfraLink'; -import chrome from 'ui/chrome'; +import * as hooks from '../../../hooks/useCore'; +import { InternalCoreStart } from 'src/core/public'; -jest - .spyOn(chrome, 'addBasePath') - .mockImplementation(path => `/basepath${path}`); +const coreMock = ({ + http: { + basePath: { + prepend: (path: string) => `/basepath${path}` + } + } +} as unknown) as InternalCoreStart; + +jest.spyOn(hooks, 'useCore').mockReturnValue(coreMock); test('InfraLink produces the correct URL', async () => { const href = await getRenderedHref( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx index 6bc2f0c355a2..eda64b4bbedb 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx @@ -7,9 +7,9 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import { compact } from 'lodash'; import React from 'react'; -import chrome from 'ui/chrome'; import url from 'url'; import { fromQuery } from './url_helpers'; +import { useCore } from '../../../hooks/useCore'; interface InfraQueryParams { time?: number; @@ -24,9 +24,10 @@ interface Props extends EuiLinkAnchorProps { } export function InfraLink({ path, query = {}, ...rest }: Props) { + const core = useCore(); const nextSearch = fromQuery(query); const href = url.format({ - pathname: chrome.addBasePath('/app/infra'), + pathname: core.http.basePath.prepend('/app/infra'), hash: compact([path, nextSearch]).join('?') }); return ; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx index e1cf2f5f4d56..24637f971bf3 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx @@ -8,16 +8,30 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../utils/testHelpers'; import { KibanaLink } from './KibanaLink'; -import chrome from 'ui/chrome'; +import * as hooks from '../../../hooks/useCore'; +import { InternalCoreStart } from 'src/core/public'; -jest - .spyOn(chrome, 'addBasePath') - .mockImplementation(path => `/basepath${path}`); +describe('KibanaLink', () => { + beforeEach(() => { + const coreMock = ({ + http: { + basePath: { + prepend: (path: string) => `/basepath${path}` + } + } + } as unknown) as InternalCoreStart; -test('KibanaLink produces the correct URL', async () => { - const href = await getRenderedHref(() => , { - search: '?rangeFrom=now-5h&rangeTo=now-2h' - } as Location); + jest.spyOn(hooks, 'useCore').mockReturnValue(coreMock); + }); - expect(href).toMatchInlineSnapshot(`"/basepath/app/kibana#/some/path"`); + afterEach(() => { + jest.resetAllMocks(); + }); + + it('produces the correct URL', async () => { + const href = await getRenderedHref(() => , { + search: '?rangeFrom=now-5h&rangeTo=now-2h' + } as Location); + expect(href).toMatchInlineSnapshot(`"/basepath/app/kibana#/some/path"`); + }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx index cc558a35bf60..53fe9da73464 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx @@ -6,8 +6,8 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import React from 'react'; -import chrome from 'ui/chrome'; import url from 'url'; +import { useCore } from '../../../hooks/useCore'; interface Props extends EuiLinkAnchorProps { path?: string; @@ -15,8 +15,9 @@ interface Props extends EuiLinkAnchorProps { } export function KibanaLink({ path, ...rest }: Props) { + const core = useCore(); const href = url.format({ - pathname: chrome.addBasePath('/app/kibana'), + pathname: core.http.basePath.prepend('/app/kibana'), hash: path }); return ; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index 0f84b3614cb4..c577a38029d2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -8,8 +8,25 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLJobLink } from './MLJobLink'; +import * as hooks from '../../../../hooks/useCore'; +import { InternalCoreStart } from 'src/core/public'; describe('MLJobLink', () => { + beforeEach(() => { + const coreMock = ({ + http: { + basePath: { + prepend: (path: string) => `/basepath${path}` + } + } + } as unknown) as InternalCoreStart; + + spyOn(hooks, 'useCore').and.returnValue(coreMock); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); it('should produce the correct URL', async () => { const href = await getRenderedHref( () => ( @@ -22,7 +39,7 @@ describe('MLJobLink', () => { ); expect(href).toEqual( - `/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))` + `/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))` ); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index ac31ac55952b..2bb4da88236c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -8,14 +8,21 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLLink } from './MLLink'; -import chrome from 'ui/chrome'; import * as savedObjects from '../../../../services/rest/savedObjects'; +import * as hooks from '../../../../hooks/useCore'; +import { InternalCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); -jest - .spyOn(chrome, 'addBasePath') - .mockImplementation(path => `/basepath${path}`); +const coreMock = ({ + http: { + basePath: { + prepend: (path: string) => `/basepath${path}` + } + } +} as unknown) as InternalCoreStart; + +jest.spyOn(hooks, 'useCore').mockReturnValue(coreMock); jest .spyOn(savedObjects, 'getAPMIndexPattern') diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx index 949c64a2171d..e0b9331d2849 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx @@ -6,11 +6,11 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import chrome from 'ui/chrome'; import url from 'url'; import rison, { RisonValue } from 'rison-node'; import { useLocation } from '../../../../hooks/useLocation'; import { getTimepickerRisonData, TimepickerRisonData } from '../rison_helpers'; +import { useCore } from '../../../../hooks/useCore'; interface MlRisonData { ml?: { @@ -25,6 +25,7 @@ interface Props { } export function MLLink({ children, path = '', query = {} }: Props) { + const core = useCore(); const location = useLocation(); const risonQuery: MlRisonData & TimepickerRisonData = getTimepickerRisonData( @@ -36,7 +37,7 @@ export function MLLink({ children, path = '', query = {} }: Props) { } const href = url.format({ - pathname: chrome.addBasePath('/app/ml'), + pathname: core.http.basePath.prepend('/app/ml'), hash: `${path}?_g=${rison.encode(risonQuery as RisonValue)}` }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/TransactionLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/TransactionLink.tsx deleted file mode 100644 index a8d387705864..000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/TransactionLink.tsx +++ /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 React from 'react'; -import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; -import { APMLink } from './APMLink'; -import { legacyEncodeURIComponent } from './url_helpers'; - -interface TransactionLinkProps { - transaction?: Transaction; -} - -export const TransactionLink: React.SFC = ({ - transaction, - children -}) => { - if (!transaction) { - return null; - } - - const serviceName = transaction.service.name; - const transactionType = legacyEncodeURIComponent( - transaction.transaction.type - ); - const traceId = transaction.trace.id; - const transactionId = transaction.transaction.id; - const name = transaction.transaction.name; - const encodedName = legacyEncodeURIComponent(name); - - return ( - - {children} - - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/APMLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/APMLink.test.tsx rename to x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx index 610f0d5302d9..a319860c75b6 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/APMLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx @@ -6,7 +6,7 @@ import { Location } from 'history'; import React from 'react'; -import { getRenderedHref } from '../../../utils/testHelpers'; +import { getRenderedHref } from '../../../../utils/testHelpers'; import { APMLink } from './APMLink'; test('APMLink should produce the correct URL', async () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/APMLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx similarity index 85% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/APMLink.tsx rename to x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx index 4e739ff8f63d..662ffea88eba 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/APMLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx @@ -8,9 +8,9 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import React from 'react'; import url from 'url'; import { pick } from 'lodash'; -import { useLocation } from '../../../hooks/useLocation'; -import { APMQueryParams, toQuery, fromQuery } from './url_helpers'; -import { TIMEPICKER_DEFAULTS } from '../../../context/UrlParamsContext/constants'; +import { useLocation } from '../../../../hooks/useLocation'; +import { APMQueryParams, toQuery, fromQuery } from '../url_helpers'; +import { TIMEPICKER_DEFAULTS } from '../../../../context/UrlParamsContext/constants'; interface Props extends EuiLinkAnchorProps { path?: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionLink.tsx new file mode 100644 index 000000000000..6fa2743fc282 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionLink.tsx @@ -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 React from 'react'; +import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; +import { APMLink } from './APMLink'; + +interface TransactionLinkProps { + transaction: Transaction | undefined; +} + +export const TransactionLink: React.SFC = ({ + transaction, + children +}) => { + if (!transaction) { + return null; + } + + const serviceName = transaction.service.name; + const traceId = transaction.trace.id; + const transactionId = transaction.transaction.id; + const transactionName = transaction.transaction.name; + const transactionType = transaction.transaction.type; + + return ( + + {children} + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx index dadb733aeb7b..ac728e72fa87 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import { toJson } from '../testHelpers'; -import { - fromQuery, - legacyDecodeURIComponent, - legacyEncodeURIComponent, - toQuery -} from './url_helpers'; +import { fromQuery, toQuery } from './url_helpers'; describe('toQuery', () => { it('should parse string to object', () => { @@ -65,64 +58,3 @@ describe('fromQuery and toQuery', () => { ).toEqual('name=john%20doe&rangeFrom=2019-03-03T12:00:00.000Z&path=a%2Fb'); }); }); - -describe('legacyEncodeURIComponent', () => { - it('should encode a string with forward slashes', () => { - expect(legacyEncodeURIComponent('a/b/c')).toBe('a~2Fb~2Fc'); - }); - - it('should encode a string with tilde', () => { - expect(legacyEncodeURIComponent('a~b~c')).toBe('a~7Eb~7Ec'); - }); - - it('should encode a string with spaces', () => { - expect(legacyEncodeURIComponent('a b c')).toBe('a~20b~20c'); - }); -}); - -describe('legacyDecodeURIComponent', () => { - ['a/b/c', 'a~b~c', 'GET /', 'foo ~ bar /'].map(input => { - it(`should encode and decode ${input}`, () => { - const converted = legacyDecodeURIComponent( - legacyEncodeURIComponent(input) - ); - expect(converted).toBe(input); - }); - }); - - describe('when Angular decodes forward slashes in a url', () => { - it('should decode value correctly', () => { - const transactionName = 'GET a/b/c/'; - const encodedTransactionName = legacyEncodeURIComponent(transactionName); - const parsedUrl = emulateAngular( - `/transaction/${encodedTransactionName}` - ); - const decodedTransactionName = legacyDecodeURIComponent( - parsedUrl.split('/')[2] - ); - - expect(decodedTransactionName).toBe(transactionName); - }); - - it('should decode value incorrectly when using vanilla encodeURIComponent', () => { - const transactionName = 'GET a/b/c/'; - const encodedTransactionName = encodeURIComponent(transactionName); - const parsedUrl = emulateAngular( - `/transaction/${encodedTransactionName}` - ); - const decodedTransactionName = decodeURIComponent( - parsedUrl.split('/')[2] - ); - - expect(decodedTransactionName).not.toBe(transactionName); - }); - }); -}); - -// Angular decodes forward slashes in path params -function emulateAngular(input: string) { - return input - .split('/') - .map(pathParam => pathParam.replace(/%2F/g, '/')) - .join('/'); -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts index df61d3de6695..0575c0837668 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -21,6 +21,8 @@ export function fromQuery(query: StringMap) { export interface APMQueryParams { transactionId?: string; + transactionName?: string; + transactionType?: string; traceId?: string; detailTab?: string; flyoutDetailTab?: string; @@ -41,19 +43,3 @@ export interface APMQueryParams { // forces every value of T[K] to be type: string type StringifyAll = { [K in keyof T]: string }; type APMQueryParamsRaw = StringifyAll; - -// This is downright horrible 😭 💔 -// Angular decodes encoded url tokens like "%2F" to "/" which causes problems when path params contains forward slashes -// This was originally fixed in Angular, but roled back to avoid breaking backwards compatability: https://github.com/angular/angular.js/commit/2bdf7126878c87474bb7588ce093d0a3c57b0026 -export function legacyEncodeURIComponent(rawUrl: string | undefined) { - return ( - rawUrl && - encodeURIComponent(rawUrl) - .replace(/~/g, '%7E') - .replace(/%/g, '~') - ); -} - -export function legacyDecodeURIComponent(encodedUrl: string | undefined) { - return encodedUrl && decodeURIComponent(encodedUrl.replace(/~/g, '%')); -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index e0b8b40e3dcf..2e3382f71b20 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -14,7 +14,6 @@ import { EuiPopover, EuiLink } from '@elastic/eui'; -import chrome from 'ui/chrome'; import url from 'url'; import { i18n } from '@kbn/i18n'; import React, { useState, FunctionComponent } from 'react'; @@ -25,6 +24,7 @@ import { DiscoverTransactionLink } from '../Links/DiscoverLinks/DiscoverTransact import { InfraLink } from '../Links/InfraLink'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { fromQuery } from '../Links/url_helpers'; +import { useCore } from '../../../hooks/useCore'; function getInfraMetricsQuery(transaction: Transaction) { const plus5 = new Date(transaction['@timestamp']); @@ -66,6 +66,8 @@ export const TransactionActionMenu: FunctionComponent = ( ) => { const { transaction } = props; + const core = useCore(); + const [isOpen, setIsOpen] = useState(false); const { urlParams } = useUrlParams(); @@ -164,7 +166,7 @@ export const TransactionActionMenu: FunctionComponent = ( ); const uptimeLink = url.format({ - pathname: chrome.addBasePath('/app/uptime'), + pathname: core.http.basePath.prepend('/app/uptime'), hash: `/?${fromQuery( pick( { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 6b4460253a0a..89adbd5c0d83 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -10,8 +10,10 @@ import 'react-testing-library/cleanup-after-each'; import { TransactionActionMenu } from '../TransactionActionMenu'; import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; import * as Transactions from './mockData'; -import * as hooks from '../../../../hooks/useAPMIndexPattern'; +import * as apmIndexPatternHooks from '../../../../hooks/useAPMIndexPattern'; +import * as coreHoooks from '../../../../hooks/useCore'; import { ISavedObject } from '../../../../services/rest/savedObjects'; +import { InternalCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); @@ -27,9 +29,18 @@ const renderTransaction = async (transaction: Record) => { describe('TransactionActionMenu component', () => { beforeEach(() => { + const coreMock = ({ + http: { + basePath: { + prepend: (path: string) => `/basepath${path}` + } + } + } as unknown) as InternalCoreStart; + jest - .spyOn(hooks, 'useAPMIndexPattern') + .spyOn(apmIndexPatternHooks, 'useAPMIndexPattern') .mockReturnValue({ id: 'foo' } as ISavedObject); + jest.spyOn(coreHoooks, 'useCore').mockReturnValue(coreMock); }); afterEach(() => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap index 10439a4a019c..63f56b8db5c5 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap @@ -25,15 +25,9 @@ exports[`TransactionActionMenu component should match the snapshot 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - - - -
    { - public getResponseTimeTickFormatter = (t: number) => { - return asMillis(t); + public getMaxY = (responseTimeSeries: TimeSeries[]) => { + const coordinates = flatten( + responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) + ); + + const numbers: number[] = coordinates.map((c: Coordinate) => + c.y ? c.y : 0 + ); + + return Math.max(...numbers, 0); + }; + + public getResponseTimeTickFormatter = (formatter: TimeFormatter) => { + return (t: number) => formatter(t); }; - public getResponseTimeTooltipFormatter = (p: Coordinate) => { - return isValidCoordinateValue(p.y) ? asMillis(p.y) : NOT_AVAILABLE_LABEL; + public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => { + return (p: Coordinate) => { + return isValidCoordinateValue(p.y) ? formatter(p.y) : NOT_AVAILABLE_LABEL; + }; }; public getTPMFormatter = (t: number) => { @@ -68,7 +88,7 @@ export class TransactionCharts extends Component { : NOT_AVAILABLE_LABEL; }; - public renderMLHeader(hasValidMlLicense: boolean) { + public renderMLHeader(hasValidMlLicense: boolean | undefined) { const { hasMLJob } = this.props; if (!hasValidMlLicense || !hasMLJob) { return null; @@ -126,6 +146,8 @@ export class TransactionCharts extends Component { const { charts, urlParams } = this.props; const { responseTimeSeries, tpmSeries } = charts; const { transactionType } = urlParams; + const maxY = this.getMaxY(responseTimeSeries); + const formatter = getTimeFormatter(maxY); return ( @@ -140,14 +162,18 @@ export class TransactionCharts extends Component { {license => - this.renderMLHeader(license.features.ml.is_available) + this.renderMLHeader( + idx(license, _ => _.features.ml.is_available) + ) } diff --git a/x-pack/legacy/plugins/apm/public/context/CoreContext.tsx b/x-pack/legacy/plugins/apm/public/context/CoreContext.tsx new file mode 100644 index 000000000000..0bf39e4d9dad --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/context/CoreContext.tsx @@ -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 React, { createContext } from 'react'; +import { InternalCoreStart } from 'src/core/public'; + +const CoreContext = createContext({} as InternalCoreStart); +const CoreProvider: React.SFC<{ core: InternalCoreStart }> = props => { + const { core, ...restProps } = props; + return ; +}; + +export { CoreContext, CoreProvider }; diff --git a/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx b/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx index 40d8f59cd924..ffd85412be0f 100644 --- a/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx +++ b/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx @@ -6,11 +6,14 @@ import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import chrome from 'ui/chrome'; - -const MANAGE_LICENSE_URL = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/license_management`; +import { useCore } from '../../hooks/useCore'; export function InvalidLicenseNotification() { + const core = useCore(); + const manageLicenseURL = core.http.basePath.prepend( + '/app/kibana#/management/elasticsearch/license_management' + ); + return ( } actions={[ - + {i18n.translate('xpack.apm.invalidLicense.licenseManagementLink', { defaultMessage: 'Manage your license' })} diff --git a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx index 38d869c983a2..c52258114e48 100644 --- a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx +++ b/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx @@ -5,15 +5,14 @@ */ import React from 'react'; import { FETCH_STATUS, useFetcher } from '../../hooks/useFetcher'; -import { loadLicense } from '../../services/rest/xpack'; +import { loadLicense, LicenseApiResponse } from '../../services/rest/xpack'; import { InvalidLicenseNotification } from './InvalidLicenseNotification'; -const initialLicense = { - features: { - watcher: { is_available: false }, - ml: { is_available: false } - }, - license: { is_active: false } +const initialLicense: LicenseApiResponse = { + features: {}, + license: { + is_active: false + } }; export const LicenseContext = React.createContext(initialLicense); diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx index a88d3f2dd33f..2604a3a12257 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx @@ -42,7 +42,9 @@ describe('UrlParamsContext', () => { }); it('should have default params', () => { - const location = { pathname: '/test/pathname' } as Location; + const location = { + pathname: '/services/opbeans-node/transactions' + } as Location; jest .spyOn(Date, 'now') @@ -52,7 +54,7 @@ describe('UrlParamsContext', () => { expect(params).toEqual({ start: '2000-06-14T12:00:00.000Z', - serviceName: 'test', + serviceName: 'opbeans-node', end: '2000-06-15T12:00:00.000Z', page: 0, processorEvent: 'transaction', diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts index 12c58d8ad54c..b1c6f6d52663 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -58,38 +58,41 @@ export function removeUndefinedProps(obj: T): Partial { export function getPathParams(pathname: string = '') { const paths = getPathAsArray(pathname); - const pageName = paths.length > 1 ? paths[1] : paths[0]; + const pageName = paths[0]; // TODO: use react router's real match params instead of guessing the path order switch (pageName) { - case 'transactions': - return { - processorEvent: 'transaction', - serviceName: paths[0], - transactionType: paths[2], - transactionName: paths[3] - }; - case 'errors': - return { - processorEvent: 'error', - serviceName: paths[0], - errorGroupId: paths[2] - }; - case 'metrics': - return { - processorEvent: 'metric', - serviceName: paths[0] - }; - case 'services': // fall thru since services and traces share path params + case 'services': + const servicePageName = paths[2]; + const serviceName = paths[1]; + switch (servicePageName) { + case 'transactions': + return { + processorEvent: 'transaction', + serviceName + }; + case 'errors': + return { + processorEvent: 'error', + serviceName, + errorGroupId: paths[3] + }; + case 'metrics': + return { + processorEvent: 'metric', + serviceName + }; + default: + return { + processorEvent: 'transaction' + }; + } + case 'traces': return { - processorEvent: 'transaction', - serviceName: undefined + processorEvent: 'transaction' }; default: - return { - processorEvent: 'transaction', - serviceName: paths[0] - }; + return {}; } } diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx index 383163e8e239..8cf326d6cfd6 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx @@ -39,17 +39,19 @@ const UrlParamsProvider: React.ComponentClass<{}> = withRouter( ({ location, children }) => { const refUrlParams = useRef(resolveUrlParams(location, {})); + const { start, end, rangeFrom, rangeTo } = refUrlParams.current; + const [, forceUpdate] = useState(''); const urlParams = useMemo( () => resolveUrlParams(location, { - start: refUrlParams.current.start, - end: refUrlParams.current.end, - rangeFrom: refUrlParams.current.rangeFrom, - rangeTo: refUrlParams.current.rangeTo + start, + end, + rangeFrom, + rangeTo }), - [location, refUrlParams.current] + [location, start, end, rangeFrom, rangeTo] ); refUrlParams.current = urlParams; diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts index 02fca270066b..234bbc55a106 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts @@ -15,10 +15,7 @@ import { toNumber, toString } from './helpers'; -import { - toQuery, - legacyDecodeURIComponent -} from '../../components/shared/Links/url_helpers'; +import { toQuery } from '../../components/shared/Links/url_helpers'; import { TIMEPICKER_DEFAULTS } from './constants'; type TimeUrlParams = Pick< @@ -27,17 +24,15 @@ type TimeUrlParams = Pick< >; export function resolveUrlParams(location: Location, state: TimeUrlParams) { - const { - processorEvent, - serviceName, - transactionName, - transactionType, - errorGroupId - } = getPathParams(location.pathname); + const { processorEvent, serviceName, errorGroupId } = getPathParams( + location.pathname + ); const { traceId, transactionId, + transactionName, + transactionType, detailTab, flyoutDetailTab, waterfallItemId, @@ -62,6 +57,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { rangeTo, refreshPaused: toBoolean(refreshPaused), refreshInterval: toNumber(refreshInterval), + // query params sortDirection, sortField, @@ -74,12 +70,14 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { flyoutDetailTab: toString(flyoutDetailTab), spanId: toNumber(spanId), kuery: kuery && decodeURIComponent(kuery), + transactionName, + transactionType, + // path params processorEvent, serviceName, - transactionType: legacyDecodeURIComponent(transactionType), - transactionName: legacyDecodeURIComponent(transactionName), errorGroupId, + // ui filters environment }); diff --git a/x-pack/legacy/plugins/apm/public/hacks/toggle_app_link_in_nav.ts b/x-pack/legacy/plugins/apm/public/hacks/toggle_app_link_in_nav.ts index 628c334863bc..295b10177754 100644 --- a/x-pack/legacy/plugins/apm/public/hacks/toggle_app_link_in_nav.ts +++ b/x-pack/legacy/plugins/apm/public/hacks/toggle_app_link_in_nav.ts @@ -6,9 +6,9 @@ import { npStart } from 'ui/new_platform'; -const apmUiEnabled = npStart.core.injectedMetadata.getInjectedVar( - 'apmUiEnabled' -); +const { core } = npStart; +const apmUiEnabled = core.injectedMetadata.getInjectedVar('apmUiEnabled'); + if (apmUiEnabled === false) { - npStart.core.chrome.navLinks.update('apm', { hidden: true }); + core.chrome.navLinks.update('apm', { hidden: true }); } diff --git a/x-pack/legacy/plugins/apm/public/hooks/useCore.tsx b/x-pack/legacy/plugins/apm/public/hooks/useCore.tsx new file mode 100644 index 000000000000..06942019d653 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/hooks/useCore.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useContext } from 'react'; +import { CoreContext } from '../context/CoreContext'; + +export function useCore() { + return useContext(CoreContext); +} diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx index 0ff770e25707..fdf0562c134d 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useContext, useEffect, useState, useMemo } from 'react'; +import React, { useContext, useEffect, useState, useMemo } from 'react'; +import { toastNotifications } from 'ui/notify'; +import { idx } from '@kbn/elastic-idx'; +import { i18n } from '@kbn/i18n'; import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext'; import { useComponentId } from './useComponentId'; +import { KFetchError } from '../../../../../../src/legacy/ui/public/kfetch/kfetch_error'; export enum FETCH_STATUS { LOADING = 'loading', @@ -16,7 +20,7 @@ export enum FETCH_STATUS { export function useFetcher( fn: () => Promise | undefined, - effectKey: any[], + fnDeps: any[], options: { preservePreviousResponse?: boolean } = {} ) { const { preservePreviousResponse = true } = options; @@ -40,11 +44,11 @@ export function useFetcher( dispatchStatus({ id, isLoading: true }); - setResult({ - data: preservePreviousResponse ? result.data : undefined, // preserve data from previous state while loading next state + setResult(prevResult => ({ + data: preservePreviousResponse ? prevResult.data : undefined, // preserve data from previous state while loading next state status: FETCH_STATUS.LOADING, error: undefined - }); + })); try { const data = await promise; @@ -57,7 +61,30 @@ export function useFetcher( }); } } catch (e) { + const err = e as KFetchError; if (!didCancel) { + toastNotifications.addWarning({ + title: i18n.translate('xpack.apm.fetcher.error.title', { + defaultMessage: `Error while fetching resource` + }), + text: ( +
    +
    + {i18n.translate('xpack.apm.fetcher.error.status', { + defaultMessage: `Error` + })} +
    + {idx(err.res, r => r.statusText)} ({idx(err.res, r => r.status)} + ) +
    + {i18n.translate('xpack.apm.fetcher.error.url', { + defaultMessage: `URL` + })} +
    + {idx(err.res, r => r.url)} +
    + ) + }); dispatchStatus({ id, isLoading: false }); setResult({ data: undefined, @@ -74,14 +101,22 @@ export function useFetcher( dispatchStatus({ id, isLoading: false }); didCancel = true; }; - }, [...effectKey, counter]); + /* eslint-disable react-hooks/exhaustive-deps */ + }, [ + counter, + id, + preservePreviousResponse, + dispatchStatus, + ...fnDeps + /* eslint-enable react-hooks/exhaustive-deps */ + ]); return useMemo( () => ({ ...result, refresh: () => { - // this will invalidate the effectKey and will result in a new request - setCounter(counter + 1); + // this will invalidate the deps to `useEffect` and will result in a new request + setCounter(count => count + 1); } }), [result] diff --git a/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts index cc4be9c519ce..0b1af3372b22 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts @@ -16,7 +16,7 @@ const INITIAL_DATA: MetricsChartsByAgentAPIResponse = { export function useServiceMetricCharts( urlParams: IUrlParams, - agentName?: string + agentName: string ) { const { serviceName, start, end } = urlParams; const uiFilters = useUiFilters(urlParams); diff --git a/x-pack/legacy/plugins/apm/public/hooks/useServiceTransactionTypes.tsx b/x-pack/legacy/plugins/apm/public/hooks/useServiceTransactionTypes.tsx new file mode 100644 index 000000000000..8a144bb178b6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/hooks/useServiceTransactionTypes.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 { loadServiceTransactionTypes } from '../services/rest/apm/services'; +import { IUrlParams } from '../context/UrlParamsContext/types'; +import { useFetcher } from './useFetcher'; + +export function useServiceTransactionTypes(urlParams: IUrlParams) { + const { serviceName, start, end } = urlParams; + const { data: transactionTypes = [] } = useFetcher(() => { + if (serviceName && start && end) { + return loadServiceTransactionTypes({ serviceName, start, end }); + } + }, [serviceName, start, end]); + + return transactionTypes; +} diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/legacy/plugins/apm/public/hooks/useTransactionBreakdown.ts index 9d6befabbead..22bfb1a1bc23 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionBreakdown.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useTransactionBreakdown.ts @@ -30,7 +30,7 @@ export function useTransactionBreakdown() { uiFilters }); } - }, [serviceName, start, end, uiFilters]); + }, [serviceName, start, end, transactionType, transactionName, uiFilters]); const receivedDataDuringLifetime = useRef(false); diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionCharts.ts b/x-pack/legacy/plugins/apm/public/hooks/useTransactionCharts.ts index c684d8d4c756..629f6bb60e1f 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionCharts.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useTransactionCharts.ts @@ -31,7 +31,7 @@ export function useTransactionCharts() { const memoizedData = useMemo( () => getTransactionCharts({ transactionType }, data), - [data] + [data, transactionType] ); return { diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts index 0d5a1ffe3477..500595dbf44b 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -12,8 +12,7 @@ import { useUiFilters } from '../context/UrlParamsContext'; const INITIAL_DATA = { buckets: [], totalHits: 0, - bucketSize: 0, - defaultSample: undefined + bucketSize: 0 }; export function useTransactionDistribution(urlParams: IUrlParams) { diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts index cda9213cf619..fc3a828dfdf7 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts @@ -5,11 +5,11 @@ */ import { useMemo } from 'react'; -import { TransactionListAPIResponse } from '../../server/lib/transactions/get_top_transactions'; import { loadTransactionList } from '../services/rest/apm/transaction_groups'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useUiFilters } from '../context/UrlParamsContext'; import { useFetcher } from './useFetcher'; +import { TransactionGroupListAPIResponse } from '../../server/lib/transaction_groups'; const getRelativeImpact = ( impact: number, @@ -21,7 +21,7 @@ const getRelativeImpact = ( 1 ); -function getWithRelativeImpact(items: TransactionListAPIResponse) { +function getWithRelativeImpact(items: TransactionGroupListAPIResponse) { const impacts = items .map(({ impact }) => impact) .filter(impact => impact !== null) as number[]; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts index 050041724a90..fd2ed152c79f 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts @@ -10,7 +10,11 @@ import { loadTrace } from '../services/rest/apm/traces'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; -const INITIAL_DATA = { trace: [], errorsPerTransaction: {} }; +const INITIAL_DATA = { + root: undefined, + trace: { items: [], exceedsMax: false }, + errorsPerTransaction: {} +}; export function useWaterfall(urlParams: IUrlParams) { const { traceId, start, end, transactionId } = urlParams; @@ -25,5 +29,5 @@ export function useWaterfall(urlParams: IUrlParams) { transactionId ]); - return { data: waterfall, status, error }; + return { data: waterfall, status, error, exceedsMax: data.trace.exceedsMax }; } diff --git a/x-pack/legacy/plugins/apm/public/index.tsx b/x-pack/legacy/plugins/apm/public/index.tsx index 86b9a5f69a8a..20d111333e4e 100644 --- a/x-pack/legacy/plugins/apm/public/index.tsx +++ b/x-pack/legacy/plugins/apm/public/index.tsx @@ -6,12 +6,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { npStart } from 'ui/new_platform'; import 'react-vis/dist/style.css'; -import { CoreStart } from 'src/core/public'; import 'ui/autoload/all'; import 'ui/autoload/styles'; import chrome from 'ui/chrome'; -import { I18nContext } from 'ui/i18n'; // @ts-ignore import { uiModules } from 'ui/modules'; import 'uiExports/autocompleteProviders'; @@ -20,10 +19,18 @@ import { plugin } from './new-platform'; import { REACT_APP_ROOT_ID } from './new-platform/plugin'; import './style/global_overrides.css'; import template from './templates/index.html'; +import { CoreProvider } from './context/CoreContext'; + +const { core } = npStart; // render APM feedback link in global help menu -chrome.helpExtension.set(domElement => { - ReactDOM.render(, domElement); +core.chrome.setHelpExtension(domElement => { + ReactDOM.render( + + + , + domElement + ); return () => { ReactDOM.unmountComponentAtNode(domElement); }; @@ -42,12 +49,6 @@ const checkForRoot = () => { } }); }; - checkForRoot().then(() => { - const core = { - i18n: { - Context: I18nContext - } - } as CoreStart; plugin().start(core); }); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index 64f240f3dc4f..abd793245cbb 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -8,8 +8,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; -import { CoreStart } from 'src/core/public'; +import { InternalCoreStart } from 'src/core/public'; import { history } from '../utils/history'; +import { CoreProvider } from '../context/CoreContext'; import { LocationProvider } from '../context/LocationContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; import { px, unit, units } from '../style/variables'; @@ -53,16 +54,18 @@ const App = () => { }; export class Plugin { - public start(core: CoreStart) { + public start(core: InternalCoreStart) { const { i18n } = core; ReactDOM.render( - - - - - - - , + + + + + + + + + , document.getElementById(REACT_APP_ROOT_ID) ); } diff --git a/x-pack/legacy/plugins/apm/public/register_feature.js b/x-pack/legacy/plugins/apm/public/register_feature.js index 0e27d1427f69..8994fac17e91 100644 --- a/x-pack/legacy/plugins/apm/public/register_feature.js +++ b/x-pack/legacy/plugins/apm/public/register_feature.js @@ -4,14 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { i18n } from '@kbn/i18n'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -if (chrome.getInjected('apmUiEnabled')) { +const { core } = npStart; +const apmUiEnabled = core.injectedMetadata.getInjectedVar('apmUiEnabled'); + +if (apmUiEnabled) { FeatureCatalogueRegistryProvider.register(() => { return { id: 'apm', diff --git a/x-pack/legacy/plugins/apm/public/services/kuery.ts b/x-pack/legacy/plugins/apm/public/services/kuery.ts deleted file mode 100644 index 6e325599f7d8..000000000000 --- a/x-pack/legacy/plugins/apm/public/services/kuery.ts +++ /dev/null @@ -1,55 +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 { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import { getAutocompleteProvider } from 'ui/autocomplete_providers'; -import { StaticIndexPattern } from 'ui/index_patterns'; -import { getFromSavedObject } from 'ui/index_patterns/static_utils'; -import { getAPMIndexPattern } from './rest/savedObjects'; - -export function convertKueryToEsQuery( - kuery: string, - indexPattern: StaticIndexPattern -) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); -} - -export async function getSuggestions( - query: string, - selectionStart: number, - apmIndexPattern: StaticIndexPattern, - boolFilter: unknown -) { - const autocompleteProvider = getAutocompleteProvider('kuery'); - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true - }; - - const getAutocompleteSuggestions = autocompleteProvider({ - config, - indexPatterns: [apmIndexPattern], - boolFilter - }); - return getAutocompleteSuggestions({ - query, - selectionStart, - selectionEnd: selectionStart - }); -} - -export async function getAPMIndexPatternForKuery(): Promise< - StaticIndexPattern | undefined -> { - const apmIndexPattern = await getAPMIndexPattern(); - if (!apmIndexPattern) { - return; - } - return getFromSavedObject(apmIndexPattern); -} diff --git a/x-pack/legacy/plugins/apm/public/services/rest/apm/error_groups.ts b/x-pack/legacy/plugins/apm/public/services/rest/apm/error_groups.ts index e4fca3c22064..2799e89070f3 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/apm/error_groups.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/apm/error_groups.ts @@ -8,7 +8,6 @@ import { ErrorDistributionAPIResponse } from '../../../../server/lib/errors/dist import { ErrorGroupAPIResponse } from '../../../../server/lib/errors/get_error_group'; import { ErrorGroupListAPIResponse } from '../../../../server/lib/errors/get_error_groups'; import { callApi } from '../callApi'; -import { getUiFiltersES } from '../../ui_filters/get_ui_filters_es'; import { UIFilters } from '../../../../typings/ui-filters'; export async function loadErrorGroupList({ @@ -33,7 +32,7 @@ export async function loadErrorGroupList({ end, sortField, sortDirection, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } @@ -56,7 +55,7 @@ export async function loadErrorGroupDetails({ query: { start, end, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } @@ -80,7 +79,7 @@ export async function loadErrorDistribution({ start, end, groupId: errorGroupId, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/apm/metrics.ts b/x-pack/legacy/plugins/apm/public/services/rest/apm/metrics.ts index 393e844ab624..a62f36478e08 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/apm/metrics.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/apm/metrics.ts @@ -6,7 +6,6 @@ import { MetricsChartsByAgentAPIResponse } from '../../../../server/lib/metrics/get_metrics_chart_data_by_agent'; import { callApi } from '../callApi'; -import { getUiFiltersES } from '../../ui_filters/get_ui_filters_es'; import { UIFilters } from '../../../../typings/ui-filters'; export async function loadMetricsChartData({ @@ -28,7 +27,7 @@ export async function loadMetricsChartData({ start, end, agentName, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/apm/services.ts b/x-pack/legacy/plugins/apm/public/services/rest/apm/services.ts index 538cdfa79fdb..045993d4fbea 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/apm/services.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/apm/services.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ServiceAPIResponse } from '../../../../server/lib/services/get_service'; +import { ServiceAgentNameAPIResponse } from '../../../../server/lib/services/get_service_agent_name'; import { ServiceListAPIResponse } from '../../../../server/lib/services/get_services'; import { callApi } from '../callApi'; -import { getUiFiltersES } from '../../ui_filters/get_ui_filters_es'; import { UIFilters } from '../../../../typings/ui-filters'; +import { ServiceTransactionTypesAPIResponse } from '../../../../server/lib/services/get_service_transaction_types'; export async function loadServiceList({ start, @@ -24,28 +24,48 @@ export async function loadServiceList({ query: { start, end, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } -export async function loadServiceDetails({ +export async function loadServiceAgentName({ serviceName, start, - end, - uiFilters + end }: { serviceName: string; start: string; end: string; - uiFilters: UIFilters; }) { - return callApi({ - pathname: `/api/apm/services/${serviceName}`, + const { agentName } = await callApi({ + pathname: `/api/apm/services/${serviceName}/agent_name`, query: { start, - end, - uiFiltersES: await getUiFiltersES(uiFilters) + end + } + }); + + return agentName; +} + +export async function loadServiceTransactionTypes({ + serviceName, + start, + end +}: { + serviceName: string; + start: string; + end: string; +}) { + const { transactionTypes } = await callApi< + ServiceTransactionTypesAPIResponse + >({ + pathname: `/api/apm/services/${serviceName}/transaction_types`, + query: { + start, + end } }); + return transactionTypes; } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/apm/traces.ts b/x-pack/legacy/plugins/apm/public/services/rest/apm/traces.ts index d2c4710fa2ec..4bcb37955027 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/apm/traces.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/apm/traces.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TraceListAPIResponse } from '../../../../server/lib/traces/get_top_traces'; import { TraceAPIResponse } from '../../../../server/lib/traces/get_trace'; import { callApi } from '../callApi'; -import { getUiFiltersES } from '../../ui_filters/get_ui_filters_es'; import { UIFilters } from '../../../../typings/ui-filters'; +import { TransactionGroupListAPIResponse } from '../../../../server/lib/transaction_groups'; export async function loadTrace({ traceId, @@ -37,12 +36,12 @@ export async function loadTraceList({ end: string; uiFilters: UIFilters; }) { - return callApi({ + return callApi({ pathname: '/api/apm/traces', query: { start, end, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/apm/transaction_groups.ts b/x-pack/legacy/plugins/apm/public/services/rest/apm/transaction_groups.ts index 7ae1b0b2ee98..9791487609ff 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/apm/transaction_groups.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/apm/transaction_groups.ts @@ -6,11 +6,10 @@ import { TransactionBreakdownAPIResponse } from '../../../../server/lib/transactions/breakdown'; import { TimeSeriesAPIResponse } from '../../../../server/lib/transactions/charts'; -import { ITransactionDistributionAPIResponse } from '../../../../server/lib/transactions/distribution'; -import { TransactionListAPIResponse } from '../../../../server/lib/transactions/get_top_transactions'; +import { TransactionDistributionAPIResponse } from '../../../../server/lib/transactions/distribution'; import { callApi } from '../callApi'; -import { getUiFiltersES } from '../../ui_filters/get_ui_filters_es'; import { UIFilters } from '../../../../typings/ui-filters'; +import { TransactionGroupListAPIResponse } from '../../../../server/lib/transaction_groups'; export async function loadTransactionList({ serviceName, @@ -25,13 +24,13 @@ export async function loadTransactionList({ transactionType: string; uiFilters: UIFilters; }) { - return await callApi({ + return await callApi({ pathname: `/api/apm/services/${serviceName}/transaction_groups`, query: { start, end, transactionType, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } @@ -55,7 +54,7 @@ export async function loadTransactionDistribution({ traceId?: string; uiFilters: UIFilters; }) { - return callApi({ + return callApi({ pathname: `/api/apm/services/${serviceName}/transaction_groups/distribution`, query: { start, @@ -64,7 +63,7 @@ export async function loadTransactionDistribution({ transactionName, transactionId, traceId, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } @@ -91,7 +90,7 @@ export async function loadTransactionCharts({ end, transactionType, transactionName, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } @@ -118,7 +117,7 @@ export async function loadTransactionBreakdown({ end, transactionName, transactionType, - uiFiltersES: await getUiFiltersES(uiFilters) + uiFilters: JSON.stringify(uiFilters) } }); } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts index 6ce6ee36fdda..c327a3bb0e5c 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { npStart } from 'ui/new_platform'; import { ESFilter } from 'elasticsearch'; -import chrome from 'ui/chrome'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -31,6 +31,8 @@ interface StartedMLJobApiResponse { jobs: MlResponseItem[]; } +const { core } = npStart; + export async function startMLJob({ serviceName, transactionType @@ -38,7 +40,9 @@ export async function startMLJob({ serviceName: string; transactionType: string; }) { - const indexPatternName = chrome.getInjected('apmIndexPatternTitle'); + const indexPatternName = core.injectedMetadata.getInjectedVar( + 'apmIndexPatternTitle' + ); const groups = ['apm', serviceName.toLowerCase()]; const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, diff --git a/x-pack/legacy/plugins/apm/public/services/rest/savedObjects.ts b/x-pack/legacy/plugins/apm/public/services/rest/savedObjects.ts index 7a80918eabac..ec0dfc12a752 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/savedObjects.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/savedObjects.ts @@ -10,6 +10,7 @@ import { callApi } from './callApi'; export interface ISavedObject { attributes: { title: string; + fields: string; }; id: string; type: string; diff --git a/x-pack/legacy/plugins/apm/public/services/rest/xpack.ts b/x-pack/legacy/plugins/apm/public/services/rest/xpack.ts index 6f9e4baef399..ff4c112c21a1 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/xpack.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/xpack.ts @@ -9,30 +9,28 @@ import { callApi } from './callApi'; export interface LicenseApiResponse { license: { - expiry_date_in_millis: number; is_active: boolean; - type: string; }; features: { - beats_management: StringMap; - graph: StringMap; - grokdebugger: StringMap; - index_management: StringMap; - logstash: StringMap; - ml: { + beats_management?: StringMap; + graph?: StringMap; + grokdebugger?: StringMap; + index_management?: StringMap; + logstash?: StringMap; + ml?: { is_available: boolean; license_type: number; has_expired: boolean; enable_links: boolean; show_links: boolean; }; - reporting: StringMap; - rollup: StringMap; - searchprofiler: StringMap; - security: StringMap; - spaces: StringMap; - tilemap: StringMap; - watcher: { + reporting?: StringMap; + rollup?: StringMap; + searchprofiler?: StringMap; + security?: StringMap; + spaces?: StringMap; + tilemap?: StringMap; + watcher?: { is_available: boolean; enable_links: boolean; show_links: boolean; diff --git a/x-pack/legacy/plugins/apm/public/services/ui_filters/get_kuery_ui_filter_es.ts b/x-pack/legacy/plugins/apm/public/services/ui_filters/get_kuery_ui_filter_es.ts deleted file mode 100644 index f2b07cfd6523..000000000000 --- a/x-pack/legacy/plugins/apm/public/services/ui_filters/get_kuery_ui_filter_es.ts +++ /dev/null @@ -1,23 +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 { ESFilter } from 'elasticsearch'; -import { convertKueryToEsQuery, getAPMIndexPatternForKuery } from '../kuery'; - -export async function getKueryUiFilterES( - kuery?: string -): Promise { - if (!kuery) { - return; - } - - const indexPattern = await getAPMIndexPatternForKuery(); - if (!indexPattern) { - return; - } - - return convertKueryToEsQuery(kuery, indexPattern) as ESFilter; -} diff --git a/x-pack/legacy/plugins/apm/public/services/ui_filters/get_ui_filters_es.ts b/x-pack/legacy/plugins/apm/public/services/ui_filters/get_ui_filters_es.ts deleted file mode 100644 index 44944736514a..000000000000 --- a/x-pack/legacy/plugins/apm/public/services/ui_filters/get_ui_filters_es.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UIFilters } from '../../../typings/ui-filters'; -import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; -import { getKueryUiFilterES } from './get_kuery_ui_filter_es'; - -export async function getUiFiltersES(uiFilters: UIFilters): Promise { - const kuery = await getKueryUiFilterES(uiFilters.kuery); - const environment = getEnvironmentUiFilterES(uiFilters.environment); - - const filters = [kuery, environment].filter(filter => !!filter); - - return encodeURIComponent(JSON.stringify(filters)); -} diff --git a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts b/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts index 678ab9009edd..093624240565 100644 --- a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts +++ b/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts @@ -22,6 +22,8 @@ describe('formatters', () => { expect(asTime(1000 * 1000)).toEqual('1,000 ms'); expect(asTime(1000 * 1000 * 10)).toEqual('10,000 ms'); expect(asTime(1000 * 1000 * 20)).toEqual('20.0 s'); + expect(asTime(60000000 * 10)).toEqual('10.0 min'); + expect(asTime(3600000000 * 1.5)).toEqual('1.5 h'); }); it('formats without unit', () => { diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters.ts index 60712bc76158..d2e82dda35c5 100644 --- a/x-pack/legacy/plugins/apm/public/utils/formatters.ts +++ b/x-pack/legacy/plugins/apm/public/utils/formatters.ts @@ -9,6 +9,8 @@ import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../common/i18n'; +const HOURS_CUT_OFF = 3600000000; // 1 hour (in microseconds) +const MINUTES_CUT_OFF = 60000000; // 1 minute (in microseconds) const SECONDS_CUT_OFF = 10 * 1000000; // 10 seconds (in microseconds) const MILLISECONDS_CUT_OFF = 10 * 1000; // 10 milliseconds (in microseconds) const SPACE = ' '; @@ -24,6 +26,38 @@ interface FormatterOptions { defaultValue?: string; } +export function asHours( + value: FormatterValue, + { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} +) { + if (value == null) { + return defaultValue; + } + const hoursLabel = + SPACE + + i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { + defaultMessage: 'h' + }); + const formatted = asDecimal(value / 3600000000); + return `${formatted}${withUnit ? hoursLabel : ''}`; +} + +export function asMinutes( + value: FormatterValue, + { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} +) { + if (value == null) { + return defaultValue; + } + const minutesLabel = + SPACE + + i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { + defaultMessage: 'min' + }); + const formatted = asDecimal(value / 60000000); + return `${formatted}${withUnit ? minutesLabel : ''}`; +} + export function asSeconds( value: FormatterValue, { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} @@ -74,13 +108,20 @@ export function asMicros( return `${formatted}${withUnit ? microsLabel : ''}`; } -type TimeFormatter = ( - max: number -) => (value: FormatterValue, options: FormatterOptions) => string; +export type TimeFormatter = ( + value: FormatterValue, + options?: FormatterOptions +) => string; + +type TimeFormatterBuilder = (max: number) => TimeFormatter; -export const getTimeFormatter: TimeFormatter = memoize((max: number) => { +export const getTimeFormatter: TimeFormatterBuilder = memoize((max: number) => { const unit = timeUnit(max); switch (unit) { + case 'h': + return asHours; + case 'm': + return asMinutes; case 's': return asSeconds; case 'ms': @@ -91,7 +132,11 @@ export const getTimeFormatter: TimeFormatter = memoize((max: number) => { }); export function timeUnit(max: number) { - if (max > SECONDS_CUT_OFF) { + if (max > HOURS_CUT_OFF) { + return 'h'; + } else if (max > MINUTES_CUT_OFF) { + return 'm'; + } else if (max > SECONDS_CUT_OFF) { return 's'; } else if (max > MILLISECONDS_CUT_OFF) { return 'ms'; diff --git a/x-pack/legacy/plugins/apm/public/services/ui_filters/__test__/get_environment_ui_filter_es.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts similarity index 86% rename from x-pack/legacy/plugins/apm/public/services/ui_filters/__test__/get_environment_ui_filter_es.test.ts rename to x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts index fa026d5505ca..df471af4f5ee 100644 --- a/x-pack/legacy/plugins/apm/public/services/ui_filters/__test__/get_environment_ui_filter_es.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/__test__/get_environment_ui_filter_es.test.ts @@ -6,8 +6,8 @@ import { ESFilter } from 'elasticsearch'; import { getEnvironmentUiFilterES } from '../get_environment_ui_filter_es'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values'; -import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; +import { SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames'; describe('getEnvironmentUiFilterES', () => { it('should return undefined, when environment is undefined', () => { diff --git a/x-pack/legacy/plugins/apm/public/services/ui_filters/get_environment_ui_filter_es.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts similarity index 78% rename from x-pack/legacy/plugins/apm/public/services/ui_filters/get_environment_ui_filter_es.ts rename to x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts index 0049437ab012..8a94e42a40b2 100644 --- a/x-pack/legacy/plugins/apm/public/services/ui_filters/get_environment_ui_filter_es.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts @@ -5,8 +5,8 @@ */ import { ESFilter } from 'elasticsearch'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; -import { SERVICE_ENVIRONMENT } from '../../../common/elasticsearch_fieldnames'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values'; +import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; export function getEnvironmentUiFilterES( environment?: string diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_kuery_ui_filter_es.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_kuery_ui_filter_es.ts new file mode 100644 index 000000000000..4ba4caa8c69d --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_kuery_ui_filter_es.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 { ESFilter } from 'elasticsearch'; +import { Server } from 'hapi'; +import { idx } from '@kbn/elastic-idx'; +import { toElasticsearchQuery, fromKueryExpression } from '@kbn/es-query'; +import { ISavedObject } from '../../../../public/services/rest/savedObjects'; +import { StaticIndexPattern } from '../../../../../../../../src/legacy/core_plugins/data/public'; +import { getAPMIndexPattern } from '../../../lib/index_pattern'; + +export async function getKueryUiFilterES( + server: Server, + kuery?: string +): Promise { + if (!kuery) { + return; + } + + const apmIndexPattern = await getAPMIndexPattern(server); + const formattedIndexPattern = getFromSavedObject(apmIndexPattern); + + if (!formattedIndexPattern) { + return; + } + + return convertKueryToEsQuery(kuery, formattedIndexPattern) as ESFilter; +} + +// lifted from src/legacy/ui/public/index_patterns/static_utils/index.js +export function getFromSavedObject(apmIndexPattern: ISavedObject) { + if (idx(apmIndexPattern, _ => _.attributes.fields) === undefined) { + return; + } + + return { + id: apmIndexPattern.id, + fields: JSON.parse(apmIndexPattern.attributes.fields), + title: apmIndexPattern.attributes.title + }; +} + +function convertKueryToEsQuery( + kuery: string, + indexPattern: StaticIndexPattern +) { + const ast = fromKueryExpression(kuery); + return toElasticsearchQuery(ast, indexPattern); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts new file mode 100644 index 000000000000..8a27799cdfe6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.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 { Server } from 'hapi'; +import { ESFilter } from 'elasticsearch'; +import { UIFilters } from '../../../../typings/ui-filters'; +import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; +import { getKueryUiFilterES } from './get_kuery_ui_filter_es'; + +export async function getUiFiltersES(server: Server, uiFilters: UIFilters) { + const kuery = await getKueryUiFilterES(server, uiFilters.kuery); + const environment = getEnvironmentUiFilterES(uiFilters.environment); + + // remove undefined items from list + const filters = [kuery, environment].filter(filter => !!filter) as ESFilter[]; + return filters; +} diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts index 420c7087c803..896c55812199 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts @@ -15,7 +15,7 @@ import { import { Legacy } from 'kibana'; import { cloneDeep, has, isString, set } from 'lodash'; import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; -import { APMRequestQuery } from './setup_request'; +import { StringMap } from '../../../typings/common'; function getApmIndices(config: Legacy.KibanaConfig) { return [ @@ -87,7 +87,7 @@ interface APMOptions { export function getESClient(req: Legacy.Request) { const cluster = req.server.plugins.elasticsearch.getCluster('data'); - const query = (req.query as unknown) as APMRequestQuery; + const query = req.query as StringMap; return { search: async ( diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/input_validation.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/input_validation.ts index aa73dd8d035c..983b569fc292 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/input_validation.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/input_validation.ts @@ -16,7 +16,7 @@ export const withDefaultValidators = ( _debug: Joi.bool(), start: dateValidation, end: dateValidation, - uiFiltersES: Joi.string(), + uiFilters: Joi.string(), ...validators }); }; diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/round_to_nearest_five_or_ten.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/round_to_nearest_five_or_ten.test.ts new file mode 100644 index 000000000000..0ba08c431844 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/round_to_nearest_five_or_ten.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 { roundToNearestFiveOrTen } from './round_to_nearest_five_or_ten'; + +describe('roundToNearestFiveOrTen', () => { + [ + { + input: 11, + output: 10 + }, + { + input: 45, + output: 50 + }, + { + input: 55, + output: 50 + }, + { + input: 400, + output: 500 + }, + { + input: 1001, + output: 1000 + }, + { + input: 2000, + output: 1000 + }, + { + input: 4000, + output: 5000 + }, + { + input: 20000, + output: 10000 + }, + { + input: 80000, + output: 100000 + } + ].forEach(({ input, output }) => { + it(`should convert ${input} to ${output}`, () => { + expect(roundToNearestFiveOrTen(input)).toBe(output); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/round_to_nearest_five_or_ten.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/round_to_nearest_five_or_ten.ts new file mode 100644 index 000000000000..7f7a07611c25 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/round_to_nearest_five_or_ten.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. + */ + +/* + * Examples: + * roundToNearestFiveOrTen(55) -> 50 + * roundToNearestFiveOrTen(95) -> 100 + * roundToNearestFiveOrTen(384) -> 500 + */ +export function roundToNearestFiveOrTen(value: number) { + const five = Math.pow(10, Math.floor(Math.log10(value))) * 5; + const ten = Math.pow(10, Math.round(Math.log10(value))); + return Math.abs(five - value) < Math.abs(ten - value) ? five : ten; +} diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts index 2dc686c896be..bd45cec316df 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -29,7 +29,7 @@ function getMockRequest() { describe('setupRequest', () => { it('should call callWithRequest with default args', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = setupRequest(mockRequest); + const { client } = await setupRequest(mockRequest); await client.search({ index: 'apm-*', body: { foo: 'bar' } }); expect(callWithRequestSpy).toHaveBeenCalledWith(mockRequest, 'search', { index: 'apm-*', @@ -50,7 +50,7 @@ describe('setupRequest', () => { describe('if index is apm-*', () => { it('should merge `observer.version_major` filter with existing boolean filters', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = setupRequest(mockRequest); + const { client } = await setupRequest(mockRequest); await client.search({ index: 'apm-*', body: { query: { bool: { filter: [{ term: 'someTerm' }] } } } @@ -70,7 +70,7 @@ describe('setupRequest', () => { it('should add `observer.version_major` filter if none exists', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = setupRequest(mockRequest); + const { client } = await setupRequest(mockRequest); await client.search({ index: 'apm-*' }); const params = callWithRequestSpy.mock.calls[0][2]; expect(params.body).toEqual({ @@ -84,7 +84,7 @@ describe('setupRequest', () => { it('should not add `observer.version_major` filter if `includeLegacyData=true`', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = setupRequest(mockRequest); + const { client } = await setupRequest(mockRequest); await client.search( { index: 'apm-*', @@ -103,7 +103,7 @@ describe('setupRequest', () => { it('if index is not an APM index, it should not add `observer.version_major` filter', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); - const { client } = setupRequest(mockRequest); + const { client } = await setupRequest(mockRequest); await client.search({ index: '.ml-*', body: { @@ -127,7 +127,7 @@ describe('setupRequest', () => { // mock includeFrozen to return false mockRequest.getUiSettingsService = () => ({ get: async () => false }); - const { client } = setupRequest(mockRequest); + const { client } = await setupRequest(mockRequest); await client.search({}); const params = callWithRequestSpy.mock.calls[0][2]; expect(params.ignore_throttled).toBe(true); @@ -138,7 +138,7 @@ describe('setupRequest', () => { // mock includeFrozen to return true mockRequest.getUiSettingsService = () => ({ get: async () => true }); - const { client } = setupRequest(mockRequest); + const { client } = await setupRequest(mockRequest); await client.search({}); const params = callWithRequestSpy.mock.calls[0][2]; expect(params.ignore_throttled).toBe(false); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index 20db3fc6bd52..4054537e04b4 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -5,29 +5,37 @@ */ import { Legacy } from 'kibana'; +import { Server } from 'hapi'; import moment from 'moment'; import { getESClient } from './es_client'; +import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; +import { PromiseReturnType } from '../../../typings/common'; -function decodeUiFiltersES(esQuery?: string) { - return esQuery ? JSON.parse(decodeURIComponent(esQuery)) : null; +function decodeUiFilters(server: Server, uiFiltersEncoded?: string) { + if (!uiFiltersEncoded) { + return []; + } + const uiFilters = JSON.parse(uiFiltersEncoded); + return getUiFiltersES(server, uiFilters); } export interface APMRequestQuery { - _debug: string; - start: string; - end: string; - uiFiltersES?: string; + _debug?: string; + start?: string; + end?: string; + uiFilters?: string; } -export type Setup = ReturnType; -export function setupRequest(req: Legacy.Request) { +export type Setup = PromiseReturnType; +export async function setupRequest(req: Legacy.Request) { const query = (req.query as unknown) as APMRequestQuery; - const config = req.server.config(); + const { server } = req; + const config = server.config(); return { start: moment.utc(query.start).valueOf(), end: moment.utc(query.end).valueOf(), - uiFiltersES: decodeUiFiltersES(query.uiFiltersES) || [], + uiFiltersES: await decodeUiFilters(server, query.uiFilters), client: getESClient(req), config }; diff --git a/x-pack/legacy/plugins/apm/server/lib/index_pattern/index.ts b/x-pack/legacy/plugins/apm/server/lib/index_pattern/index.ts index aa9e27ed2442..0b9407b288b1 100644 --- a/x-pack/legacy/plugins/apm/server/lib/index_pattern/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/index_pattern/index.ts @@ -3,12 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { InternalCoreSetup } from 'src/core/server'; +import { Server } from 'hapi'; import { getSavedObjectsClient } from '../helpers/saved_objects_client'; import apmIndexPattern from '../../../../../../../src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json'; -export async function getIndexPattern(core: InternalCoreSetup) { - const { server } = core.http; +export async function getAPMIndexPattern(server: Server) { const config = server.config(); const apmIndexPatternTitle = config.get('apm_oss.indexPattern'); const savedObjectsClient = getSavedObjectsClient(server); diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service.ts deleted file mode 100644 index d39397fedd15..000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_service.ts +++ /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 { idx } from '@kbn/elastic-idx'; -import { - PROCESSOR_EVENT, - SERVICE_AGENT_NAME, - SERVICE_NAME, - TRANSACTION_TYPE -} from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; -import { rangeFilter } from '../helpers/range_filter'; -import { Setup } from '../helpers/setup_request'; - -export type ServiceAPIResponse = PromiseReturnType; -export async function getService(serviceName: string, setup: Setup) { - const { start, end, uiFiltersES, client, config } = setup; - - const params = { - index: [ - config.get('apm_oss.errorIndices'), - config.get('apm_oss.transactionIndices') - ], - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { terms: { [PROCESSOR_EVENT]: ['error', 'transaction'] } }, - { range: rangeFilter(start, end) }, - ...uiFiltersES - ] - } - }, - aggs: { - types: { - terms: { field: TRANSACTION_TYPE, size: 100 } - }, - agents: { - terms: { field: SERVICE_AGENT_NAME, size: 1 } - } - } - } - }; - - const { aggregations } = await client.search(params); - const buckets = idx(aggregations, _ => _.types.buckets) || []; - const types = buckets.map(bucket => bucket.key); - const agentName = idx(aggregations, _ => _.agents.buckets[0].key) || ''; - - return { - serviceName, - types, - agentName - }; -} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts new file mode 100644 index 000000000000..ebe0ba9827b5 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.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 { idx } from '@kbn/elastic-idx'; +import { + PROCESSOR_EVENT, + SERVICE_AGENT_NAME, + SERVICE_NAME +} from '../../../common/elasticsearch_fieldnames'; +import { PromiseReturnType } from '../../../typings/common'; +import { rangeFilter } from '../helpers/range_filter'; +import { Setup } from '../helpers/setup_request'; + +export type ServiceAgentNameAPIResponse = PromiseReturnType< + typeof getServiceAgentName +>; +export async function getServiceAgentName(serviceName: string, setup: Setup) { + const { start, end, client, config } = setup; + + const params = { + terminate_after: 1, + index: [ + config.get('apm_oss.errorIndices'), + config.get('apm_oss.transactionIndices'), + config.get('apm_oss.metricsIndices') + ], + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { + terms: { [PROCESSOR_EVENT]: ['error', 'transaction', 'metric'] } + }, + { range: rangeFilter(start, end) } + ] + } + }, + aggs: { + agents: { + terms: { field: SERVICE_AGENT_NAME, size: 1 } + } + } + } + }; + + const { aggregations } = await client.search(params); + const agentName = idx(aggregations, _ => _.agents.buckets[0].key); + return { agentName }; +} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts new file mode 100644 index 000000000000..c8053c57776d --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { idx } from '@kbn/elastic-idx'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_TYPE +} from '../../../common/elasticsearch_fieldnames'; +import { PromiseReturnType } from '../../../typings/common'; +import { rangeFilter } from '../helpers/range_filter'; +import { Setup } from '../helpers/setup_request'; + +export type ServiceTransactionTypesAPIResponse = PromiseReturnType< + typeof getServiceTransactionTypes +>; +export async function getServiceTransactionTypes( + serviceName: string, + setup: Setup +) { + const { start, end, client, config } = setup; + + const params = { + index: [config.get('apm_oss.transactionIndices')], + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, + { range: rangeFilter(start, end) } + ] + } + }, + aggs: { + types: { + terms: { field: TRANSACTION_TYPE, size: 100 } + } + } + } + }; + + const { aggregations } = await client.search(params); + const buckets = idx(aggregations, _ => _.types.buckets) || []; + const transactionTypes = buckets.map(bucket => bucket.key); + return { transactionTypes }; +} diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_top_traces.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_top_traces.ts deleted file mode 100644 index 17a8b1175972..000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/traces/get_top_traces.ts +++ /dev/null @@ -1,35 +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 { - PARENT_ID, - PROCESSOR_EVENT, - TRANSACTION_SAMPLED -} from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; -import { rangeFilter } from '../helpers/range_filter'; -import { Setup } from '../helpers/setup_request'; -import { getTransactionGroups } from '../transaction_groups'; - -export type TraceListAPIResponse = PromiseReturnType; -export async function getTopTraces(setup: Setup) { - const { start, end, uiFiltersES } = setup; - - const bodyQuery = { - bool: { - // no parent ID means this transaction is a "root" transaction, i.e. a trace - must_not: { exists: { field: PARENT_ID } }, - filter: [ - { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - ...uiFiltersES - ], - should: [{ term: { [TRANSACTION_SAMPLED]: true } }] - } - }; - - return getTransactionGroups(setup, bodyQuery); -} diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts index 11599d09c1d6..00eeefb4b4fc 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts @@ -7,7 +7,10 @@ import { SearchParams } from 'elasticsearch'; import { PROCESSOR_EVENT, - TRACE_ID + TRACE_ID, + PARENT_ID, + TRANSACTION_DURATION, + SPAN_DURATION } from '../../../common/elasticsearch_fieldnames'; import { Span } from '../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../typings/es_schemas/ui/Transaction'; @@ -16,6 +19,7 @@ import { Setup } from '../helpers/setup_request'; export async function getTraceItems(traceId: string, setup: Setup) { const { start, end, client, config } = setup; + const maxTraceItems = config.get('xpack.apm.ui.maxTraceItems'); const params: SearchParams = { index: [ @@ -23,20 +27,31 @@ export async function getTraceItems(traceId: string, setup: Setup) { config.get('apm_oss.transactionIndices') ], body: { - size: 1000, + size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, { terms: { [PROCESSOR_EVENT]: ['span', 'transaction'] } }, { range: rangeFilter(start, end) } - ] + ], + should: { + exists: { field: PARENT_ID } + } } - } + }, + sort: [ + { _score: { order: 'asc' } }, + { [TRANSACTION_DURATION]: { order: 'desc' } }, + { [SPAN_DURATION]: { order: 'desc' } } + ] } }; const resp = await client.search(params); - return resp.hits.hits.map(hit => hit._source); + return { + items: resp.hits.hits.map(hit => hit._source), + exceedsMax: resp.hits.total > maxTraceItems + }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap index e11d0e90db0d..c55e54938aab 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`transactionGroupsFetcher should call client with correct query 1`] = ` +exports[`transactionGroupsFetcher type: top_traces should call client.search with correct query 1`] = ` Array [ Array [ Object { @@ -52,7 +52,145 @@ Array [ }, }, "query": Object { - "my": "bodyQuery", + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.environment": "test", + }, + }, + ], + "must_not": Array [ + Object { + "exists": Object { + "field": "parent.id", + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", + }, + ], +] +`; + +exports[`transactionGroupsFetcher type: top_transactions should call client.search with correct query 1`] = ` +Array [ + Array [ + Object { + "body": Object { + "aggs": Object { + "transactions": Object { + "aggs": Object { + "avg": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + "p95": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 95, + ], + }, + }, + "sample": Object { + "top_hits": Object { + "size": 1, + "sort": Array [ + Object { + "_score": "desc", + }, + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "sum": Object { + "sum": Object { + "field": "transaction.duration.us", + }, + }, + }, + "terms": Object { + "field": "transaction.name", + "order": Object { + "sum": "desc", + }, + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "service.environment": "test", + }, + }, + Object { + "term": Object { + "service.name": "opbeans-node", + }, + }, + Object { + "term": Object { + "transaction.type": "request", + }, + }, + ], + "must_not": Array [], + "should": Array [ + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + }, }, "size": 0, }, diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts index 0f21b3b0206d..fc7b1b412789 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts @@ -4,42 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESResponse, transactionGroupsFetcher } from './fetcher'; +import { transactionGroupsFetcher } from './fetcher'; -describe('transactionGroupsFetcher', () => { - let res: ESResponse; - let clientSpy: jest.Mock; - beforeEach(async () => { - clientSpy = jest.fn().mockResolvedValue('ES response'); - - const setup = { - start: 1528113600000, - end: 1528977600000, - client: { - search: clientSpy - } as any, - config: { - get: jest.fn((key: string) => { - switch (key) { - case 'apm_oss.transactionIndices': - return 'myIndex'; - case 'xpack.apm.ui.transactionGroupBucketSize': - return 100; - } - }), - has: () => true - }, - uiFiltersES: [{ term: { 'service.environment': 'test' } }] - }; - const bodyQuery = { my: 'bodyQuery' }; - res = await transactionGroupsFetcher(setup, bodyQuery); - }); +function getSetup() { + return { + start: 1528113600000, + end: 1528977600000, + client: { + search: jest.fn() + } as any, + config: { + get: jest.fn((key: string) => { + switch (key) { + case 'apm_oss.transactionIndices': + return 'myIndex'; + case 'xpack.apm.ui.transactionGroupBucketSize': + return 100; + } + }), + has: () => true + }, + uiFiltersES: [{ term: { 'service.environment': 'test' } }] + }; +} - it('should call client with correct query', () => { - expect(clientSpy.mock.calls).toMatchSnapshot(); +describe('transactionGroupsFetcher', () => { + describe('type: top_traces', () => { + it('should call client.search with correct query', async () => { + const setup = getSetup(); + await transactionGroupsFetcher({ type: 'top_traces' }, setup); + expect(setup.client.search.mock.calls).toMatchSnapshot(); + }); }); - it('should return correct response', () => { - expect(res).toBe('ES response'); + describe('type: top_transactions', () => { + it('should call client.search with correct query', async () => { + const setup = getSetup(); + await transactionGroupsFetcher( + { + type: 'top_transactions', + serviceName: 'opbeans-node', + transactionType: 'request' + }, + setup + ); + expect(setup.client.search.mock.calls).toMatchSnapshot(); + }); }); }); diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts index b909d5ff62a7..3b32776739c8 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -6,19 +6,60 @@ import { TRANSACTION_DURATION, - TRANSACTION_NAME + TRANSACTION_NAME, + PROCESSOR_EVENT, + PARENT_ID, + TRANSACTION_SAMPLED, + SERVICE_NAME, + TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType, StringMap } from '../../../typings/common'; +import { PromiseReturnType } from '../../../typings/common'; import { Setup } from '../helpers/setup_request'; +import { rangeFilter } from '../helpers/range_filter'; +import { BoolQuery } from '../../../typings/elasticsearch'; + +interface TopTransactionOptions { + type: 'top_transactions'; + serviceName: string; + transactionType: string; +} + +interface TopTraceOptions { + type: 'top_traces'; +} + +export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; -export function transactionGroupsFetcher(setup: Setup, bodyQuery: StringMap) { - const { client, config } = setup; +export function transactionGroupsFetcher(options: Options, setup: Setup) { + const { client, config, start, end, uiFiltersES } = setup; + + const bool: BoolQuery = { + must_not: [], + // prefer sampled transactions + should: [{ term: { [TRANSACTION_SAMPLED]: true } }], + filter: [ + { range: rangeFilter(start, end) }, + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + ...uiFiltersES + ] + }; + + if (options.type === 'top_traces') { + // A transaction without `parent.id` is considered a "root" transaction, i.e. a trace + bool.must_not.push({ exists: { field: PARENT_ID } }); + } else { + bool.filter.push({ term: { [SERVICE_NAME]: options.serviceName } }); + bool.filter.push({ term: { [TRANSACTION_TYPE]: options.transactionType } }); + } + const params = { index: config.get('apm_oss.transactionIndices'), body: { size: 0, - query: bodyQuery, + query: { + bool + }, aggs: { transactions: { terms: { diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/index.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/index.ts index 934495ea83c2..73e30f28c420 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/index.ts @@ -4,14 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StringMap } from '../../../typings/common'; import { Setup } from '../helpers/setup_request'; -import { transactionGroupsFetcher } from './fetcher'; +import { transactionGroupsFetcher, Options } from './fetcher'; import { transactionGroupsTransformer } from './transform'; +import { PromiseReturnType } from '../../../typings/common'; -export async function getTransactionGroups(setup: Setup, bodyQuery: StringMap) { +export type TransactionGroupListAPIResponse = PromiseReturnType< + typeof getTransactionGroupList +>; +export async function getTransactionGroupList(options: Options, setup: Setup) { const { start, end } = setup; - const response = await transactionGroupsFetcher(setup, bodyQuery); + const response = await transactionGroupsFetcher(options, setup); return transactionGroupsTransformer({ response, start, diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts index 3f09aa55e3f0..bebe20824360 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -79,26 +79,14 @@ export async function getTransactionBreakdown({ }; const filters = [ - { - term: { - [SERVICE_NAME]: { - value: serviceName - } - } - }, - { - term: { - [TRANSACTION_TYPE]: { - value: transactionType - } - } - }, + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, { range: rangeFilter(start, end) }, ...uiFiltersES ]; if (transactionName) { - filters.push({ term: { [TRANSACTION_NAME]: { value: transactionName } } }); + filters.push({ term: { [TRANSACTION_NAME]: transactionName } }); } const params = { @@ -107,7 +95,7 @@ export async function getTransactionBreakdown({ size: 0, query: { bool: { - must: filters + filter: filters } }, aggs: { diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts index 79ae39c693b7..07f1d9661819 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts @@ -20,6 +20,7 @@ describe('getAnomalySeries', () => { avgAnomalies = await getAnomalySeries({ serviceName: 'myServiceName', transactionType: 'myTransactionType', + transactionName: undefined, timeSeriesDates: [100, 100000], setup: { start: 0, diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index 7b5b77e2a2dd..b55d264cfbbf 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -18,8 +18,8 @@ export async function getAnomalySeries({ setup }: { serviceName: string; - transactionType?: string; - transactionName?: string; + transactionType: string | undefined; + transactionName: string | undefined; timeSeriesDates: number[]; setup: Setup; }) { diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 9ce664fa21e7..dc5cb925b720 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -26,8 +26,8 @@ export function timeseriesFetcher({ setup }: { serviceName: string; - transactionType?: string; - transactionName?: string; + transactionType: string | undefined; + transactionName: string | undefined; setup: Setup; }) { const { start, end, uiFiltersES, client, config } = setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index e2a292d1bf12..6c18ab84cdfa 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -11,8 +11,8 @@ import { timeseriesTransformer } from './transform'; export async function getApmTimeseriesData(options: { serviceName: string; - transactionType?: string; - transactionName?: string; + transactionType: string | undefined; + transactionName: string | undefined; setup: Setup; }) { const { start, end } = options.setup; diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/index.ts index f1ded4dbaffa..c297f3a050f0 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/index.ts @@ -19,8 +19,8 @@ export type TimeSeriesAPIResponse = PromiseReturnType< >; export async function getTransactionCharts(options: { serviceName: string; - transactionType?: string; - transactionName?: string; + transactionType: string | undefined; + transactionName: string | undefined; setup: Setup; }) { const apmTimeseries = await getApmTimeseriesData(options); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts deleted file mode 100644 index 20f69a0bd4d8..000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts +++ /dev/null @@ -1,65 +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 { - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, - TRANSACTION_NAME, - TRANSACTION_TYPE -} from '../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../helpers/setup_request'; - -export async function calculateBucketSize( - serviceName: string, - transactionName: string, - transactionType: string, - setup: Setup -) { - const { start, end, uiFiltersES, client, config } = setup; - - const params = { - index: config.get('apm_oss.transactionIndices'), - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [TRANSACTION_NAME]: transactionName } }, - { - range: { - '@timestamp': { - gte: start, - lte: end, - format: 'epoch_millis' - } - } - }, - ...uiFiltersES - ] - } - }, - aggs: { - stats: { - extended_stats: { - field: TRANSACTION_DURATION - } - } - } - } - }; - - const resp = await client.search(params); - - const minBucketSize: number = config.get('xpack.apm.minimumBucketSize'); - const bucketTargetCount: number = config.get('xpack.apm.bucketTargetCount'); - const max = resp.aggregations.stats.max; - const bucketSize = Math.floor(max / bucketTargetCount); - return bucketSize > minBucketSize ? bucketSize : minBucketSize; -} diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index d265aa5173d2..458aad225fd9 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -23,11 +23,11 @@ export function bucketFetcher( transactionType: string, transactionId: string, traceId: string, + distributionMax: number, bucketSize: number, setup: Setup ) { const { start, end, uiFiltersES, client, config } = setup; - const bucketTargetCount = config.get('xpack.apm.bucketTargetCount'); const params = { index: config.get('apm_oss.transactionIndices'), @@ -58,7 +58,7 @@ export function bucketFetcher( min_doc_count: 0, extended_bounds: { min: 0, - max: bucketSize * bucketTargetCount + max: distributionMax } }, aggs: { diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index f5cc252fc68f..86429986063e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -14,6 +14,7 @@ export async function getBuckets( transactionType: string, transactionId: string, traceId: string, + distributionMax: number, bucketSize: number, setup: Setup ) { @@ -23,6 +24,7 @@ export async function getBuckets( transactionType, transactionId, traceId, + distributionMax, bucketSize, setup ); diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts index 44ee59dcaa7c..09992f08d056 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash'; import { idx } from '@kbn/elastic-idx'; import { PromiseReturnType } from '../../../../../typings/common'; import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; @@ -12,19 +11,6 @@ import { bucketFetcher } from './fetcher'; type DistributionBucketResponse = PromiseReturnType; -function getDefaultSample(buckets: IBucket[]) { - const samples = buckets - .filter(bucket => bucket.count > 0 && bucket.sample) - .map(bucket => bucket.sample); - - if (isEmpty(samples)) { - return; - } - - const middleIndex = Math.floor(samples.length / 2); - return samples[middleIndex]; -} - export type IBucket = ReturnType; function getBucket( bucket: DistributionBucketResponse['aggregations']['distribution']['buckets'][0] @@ -50,7 +36,6 @@ export function bucketTransformer(response: DistributionBucketResponse) { return { totalHits: response.hits.total, - buckets, - defaultSample: getDefaultSample(buckets) + buckets }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts new file mode 100644 index 000000000000..01a186e74ecc --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -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 { + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_DURATION, + TRANSACTION_NAME, + TRANSACTION_TYPE +} from '../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../helpers/setup_request'; + +export async function getDistributionMax( + serviceName: string, + transactionName: string, + transactionType: string, + setup: Setup +) { + const { start, end, uiFiltersES, client, config } = setup; + + const params = { + index: config.get('apm_oss.transactionIndices'), + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + { term: { [TRANSACTION_NAME]: transactionName } }, + { + range: { + '@timestamp': { + gte: start, + lte: end, + format: 'epoch_millis' + } + } + }, + ...uiFiltersES + ] + } + }, + aggs: { + stats: { + extended_stats: { + field: TRANSACTION_DURATION + } + } + } + } + }; + + const resp = await client.search(params); + return resp.aggregations.stats.max; +} diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts index e02d22d0d530..454e247a19ca 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts @@ -6,10 +6,22 @@ import { PromiseReturnType } from '../../../../typings/common'; import { Setup } from '../../helpers/setup_request'; -import { calculateBucketSize } from './calculate_bucket_size'; import { getBuckets } from './get_buckets'; +import { getDistributionMax } from './get_distribution_max'; +import { roundToNearestFiveOrTen } from '../../helpers/round_to_nearest_five_or_ten'; -export type ITransactionDistributionAPIResponse = PromiseReturnType< +function getBucketSize(max: number, { config }: Setup) { + const minBucketSize: number = config.get( + 'xpack.apm.minimumBucketSize' + ); + const bucketTargetCount = config.get('xpack.apm.bucketTargetCount'); + const bucketSize = max / bucketTargetCount; + return roundToNearestFiveOrTen( + bucketSize > minBucketSize ? bucketSize : minBucketSize + ); +} + +export type TransactionDistributionAPIResponse = PromiseReturnType< typeof getTransactionDistribution >; export async function getTransactionDistribution({ @@ -27,19 +39,25 @@ export async function getTransactionDistribution({ traceId: string; setup: Setup; }) { - const bucketSize = await calculateBucketSize( + const distributionMax = await getDistributionMax( serviceName, transactionName, transactionType, setup ); - const { defaultSample, buckets, totalHits } = await getBuckets( + if (distributionMax == null) { + return { totalHits: 0, buckets: [], bucketSize: 0 }; + } + + const bucketSize = getBucketSize(distributionMax, setup); + const { buckets, totalHits } = await getBuckets( serviceName, transactionName, transactionType, transactionId, traceId, + distributionMax, bucketSize, setup ); @@ -47,7 +65,6 @@ export async function getTransactionDistribution({ return { totalHits, buckets, - bucketSize, - defaultSample + bucketSize }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/get_top_transactions/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/get_top_transactions/index.ts deleted file mode 100644 index f2b15ed924cf..000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/get_top_transactions/index.ts +++ /dev/null @@ -1,51 +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 { - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_TYPE -} from '../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../typings/common'; -import { rangeFilter } from '../../helpers/range_filter'; -import { Setup } from '../../helpers/setup_request'; -import { getTransactionGroups } from '../../transaction_groups'; - -export interface IOptions { - setup: Setup; - transactionType?: string; - serviceName: string; -} - -export type TransactionListAPIResponse = PromiseReturnType< - typeof getTopTransactions ->; -export async function getTopTransactions({ - setup, - transactionType, - serviceName -}: IOptions) { - const { start, end, uiFiltersES } = setup; - - const bodyQuery = { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - { range: rangeFilter(start, end) }, - ...uiFiltersES - ] - } - }; - - if (transactionType) { - bodyQuery.bool.filter.push({ - term: { [TRANSACTION_TYPE]: transactionType } - }); - } - - return getTransactionGroups(setup, bodyQuery); -} diff --git a/x-pack/legacy/plugins/apm/server/routes/errors.ts b/x-pack/legacy/plugins/apm/server/routes/errors.ts index b4b7f3fdc1a8..1afdca73299f 100644 --- a/x-pack/legacy/plugins/apm/server/routes/errors.ts +++ b/x-pack/legacy/plugins/apm/server/routes/errors.ts @@ -33,8 +33,8 @@ export function initErrorsApi(core: InternalCoreSetup) { }, tags: ['access:apm'] }, - handler: req => { - const setup = setupRequest(req); + handler: async req => { + const setup = await setupRequest(req); const { serviceName } = req.params; const { sortField, sortDirection } = req.query as { sortField: string; @@ -59,8 +59,8 @@ export function initErrorsApi(core: InternalCoreSetup) { }, tags: ['access:apm'] }, - handler: req => { - const setup = setupRequest(req); + handler: async req => { + const setup = await setupRequest(req); const { serviceName, groupId } = req.params; return getErrorGroup({ serviceName, groupId, setup }).catch( defaultErrorHandler @@ -79,8 +79,8 @@ export function initErrorsApi(core: InternalCoreSetup) { }, tags: ['access:apm'] }, - handler: req => { - const setup = setupRequest(req); + handler: async req => { + const setup = await setupRequest(req); const { serviceName } = req.params; const { groupId } = req.query as { groupId?: string }; return getErrorDistribution({ serviceName, groupId, setup }).catch( diff --git a/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts b/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts index 20ae3c336366..12499b833173 100644 --- a/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts @@ -6,9 +6,8 @@ import Boom from 'boom'; import { InternalCoreSetup } from 'src/core/server'; -import { getIndexPattern } from '../lib/index_pattern'; +import { getAPMIndexPattern } from '../lib/index_pattern'; -const ROOT = '/api/apm/index_pattern'; const defaultErrorHandler = (err: Error & { status?: number }) => { // eslint-disable-next-line console.error(err.stack); @@ -19,12 +18,12 @@ export function initIndexPatternApi(core: InternalCoreSetup) { const { server } = core.http; server.route({ method: 'GET', - path: ROOT, + path: '/api/apm/index_pattern', options: { tags: ['access:apm'] }, handler: async req => { - return await getIndexPattern(core).catch(defaultErrorHandler); + return await getAPMIndexPattern(server).catch(defaultErrorHandler); } }); } diff --git a/x-pack/legacy/plugins/apm/server/routes/metrics.ts b/x-pack/legacy/plugins/apm/server/routes/metrics.ts index 16cc8b8a652f..55b6bdbae3b3 100644 --- a/x-pack/legacy/plugins/apm/server/routes/metrics.ts +++ b/x-pack/legacy/plugins/apm/server/routes/metrics.ts @@ -32,7 +32,7 @@ export function initMetricsApi(core: InternalCoreSetup) { tags: ['access:apm'] }, handler: async req => { - const setup = setupRequest(req); + const setup = await setupRequest(req); const { serviceName } = req.params; // casting approach recommended here: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/25605 const { agentName } = req.query as { agentName: string }; diff --git a/x-pack/legacy/plugins/apm/server/routes/services.ts b/x-pack/legacy/plugins/apm/server/routes/services.ts index 063bf0556daf..c6143f522f38 100644 --- a/x-pack/legacy/plugins/apm/server/routes/services.ts +++ b/x-pack/legacy/plugins/apm/server/routes/services.ts @@ -10,8 +10,9 @@ import { AgentName } from '../../typings/es_schemas/ui/fields/Agent'; import { createApmTelementry, storeApmTelemetry } from '../lib/apm_telemetry'; import { withDefaultValidators } from '../lib/helpers/input_validation'; import { setupRequest } from '../lib/helpers/setup_request'; -import { getService } from '../lib/services/get_service'; +import { getServiceAgentName } from '../lib/services/get_service_agent_name'; import { getServices } from '../lib/services/get_services'; +import { getServiceTransactionTypes } from '../lib/services/get_service_transaction_types'; const ROOT = '/api/apm/services'; const defaultErrorHandler = (err: Error) => { @@ -32,7 +33,7 @@ export function initServicesApi(core: InternalCoreSetup) { tags: ['access:apm'] }, handler: async req => { - const setup = setupRequest(req); + const setup = await setupRequest(req); const services = await getServices(setup).catch(defaultErrorHandler); // Store telemetry data derived from services @@ -48,17 +49,35 @@ export function initServicesApi(core: InternalCoreSetup) { server.route({ method: 'GET', - path: `${ROOT}/{serviceName}`, + path: `${ROOT}/{serviceName}/agent_name`, options: { validate: { query: withDefaultValidators() }, tags: ['access:apm'] }, - handler: req => { - const setup = setupRequest(req); + handler: async req => { + const setup = await setupRequest(req); const { serviceName } = req.params; - return getService(serviceName, setup).catch(defaultErrorHandler); + return getServiceAgentName(serviceName, setup).catch(defaultErrorHandler); + } + }); + + server.route({ + method: 'GET', + path: `${ROOT}/{serviceName}/transaction_types`, + options: { + validate: { + query: withDefaultValidators() + }, + tags: ['access:apm'] + }, + handler: async req => { + const setup = await setupRequest(req); + const { serviceName } = req.params; + return getServiceTransactionTypes(serviceName, setup).catch( + defaultErrorHandler + ); } }); } diff --git a/x-pack/legacy/plugins/apm/server/routes/settings.ts b/x-pack/legacy/plugins/apm/server/routes/settings.ts index 570583e05088..c23ead9d4498 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings.ts @@ -42,7 +42,7 @@ export function initSettingsApi(core: InternalCoreSetup) { handler: async req => { await createApmAgentConfigurationIndex(server); - const setup = setupRequest(req); + const setup = await setupRequest(req); return await listConfigurations({ setup }).catch(defaultErrorHandler); @@ -62,7 +62,7 @@ export function initSettingsApi(core: InternalCoreSetup) { tags: ['access:apm'] }, handler: async req => { - const setup = setupRequest(req); + const setup = await setupRequest(req); const { configurationId } = req.params; return await deleteConfiguration({ configurationId, @@ -84,7 +84,7 @@ export function initSettingsApi(core: InternalCoreSetup) { tags: ['access:apm'] }, handler: async req => { - const setup = setupRequest(req); + const setup = await setupRequest(req); return await getServiceNames({ setup }).catch(defaultErrorHandler); @@ -104,7 +104,7 @@ export function initSettingsApi(core: InternalCoreSetup) { tags: ['access:apm'] }, handler: async req => { - const setup = setupRequest(req); + const setup = await setupRequest(req); const { serviceName } = req.params; return await getEnvironments({ serviceName, @@ -142,7 +142,7 @@ export function initSettingsApi(core: InternalCoreSetup) { tags: ['access:apm'] }, handler: async req => { - const setup = setupRequest(req); + const setup = await setupRequest(req); const configuration = req.payload as AgentConfigurationIntake; return await createConfiguration({ configuration, @@ -165,7 +165,7 @@ export function initSettingsApi(core: InternalCoreSetup) { tags: ['access:apm'] }, handler: async req => { - const setup = setupRequest(req); + const setup = await setupRequest(req); const { configurationId } = req.params; const configuration = req.payload as AgentConfigurationIntake; return await updateConfiguration({ @@ -202,7 +202,7 @@ export function initSettingsApi(core: InternalCoreSetup) { await createApmAgentConfigurationIndex(server); - const setup = setupRequest(req); + const setup = await setupRequest(req); const payload = req.payload as Payload; const serviceName = payload.service.name; const environment = payload.service.environment; diff --git a/x-pack/legacy/plugins/apm/server/routes/traces.ts b/x-pack/legacy/plugins/apm/server/routes/traces.ts index a69361903e6b..3b32179650e6 100644 --- a/x-pack/legacy/plugins/apm/server/routes/traces.ts +++ b/x-pack/legacy/plugins/apm/server/routes/traces.ts @@ -5,12 +5,11 @@ */ import Boom from 'boom'; - import { InternalCoreSetup } from 'src/core/server'; import { withDefaultValidators } from '../lib/helpers/input_validation'; import { setupRequest } from '../lib/helpers/setup_request'; -import { getTopTraces } from '../lib/traces/get_top_traces'; import { getTrace } from '../lib/traces/get_trace'; +import { getTransactionGroupList } from '../lib/transaction_groups'; const ROOT = '/api/apm/traces'; const defaultErrorHandler = (err: Error) => { @@ -32,10 +31,11 @@ export function initTracesApi(core: InternalCoreSetup) { }, tags: ['access:apm'] }, - handler: req => { - const setup = setupRequest(req); - - return getTopTraces(setup).catch(defaultErrorHandler); + handler: async req => { + const setup = await setupRequest(req); + return getTransactionGroupList({ type: 'top_traces' }, setup).catch( + defaultErrorHandler + ); } }); @@ -49,9 +49,9 @@ export function initTracesApi(core: InternalCoreSetup) { }, tags: ['access:apm'] }, - handler: req => { + handler: async req => { const { traceId } = req.params; - const setup = setupRequest(req); + const setup = await setupRequest(req); return getTrace(traceId, setup).catch(defaultErrorHandler); } }); diff --git a/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts b/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts index 0d93fd895c76..7fab69be1af6 100644 --- a/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts @@ -11,8 +11,8 @@ import { withDefaultValidators } from '../lib/helpers/input_validation'; import { setupRequest } from '../lib/helpers/setup_request'; import { getTransactionCharts } from '../lib/transactions/charts'; import { getTransactionDistribution } from '../lib/transactions/distribution'; -import { getTopTransactions } from '../lib/transactions/get_top_transactions'; import { getTransactionBreakdown } from '../lib/transactions/breakdown'; +import { getTransactionGroupList } from '../lib/transaction_groups'; const defaultErrorHandler = (err: Error) => { // eslint-disable-next-line @@ -34,16 +34,19 @@ export function initTransactionGroupsApi(core: InternalCoreSetup) { }, tags: ['access:apm'] }, - handler: req => { + handler: async req => { const { serviceName } = req.params; - const { transactionType } = req.query as { transactionType?: string }; - const setup = setupRequest(req); + const { transactionType } = req.query as { transactionType: string }; + const setup = await setupRequest(req); - return getTopTransactions({ - serviceName, - transactionType, + return getTransactionGroupList( + { + type: 'top_transactions', + serviceName, + transactionType + }, setup - }).catch(defaultErrorHandler); + ).catch(defaultErrorHandler); } }); @@ -59,8 +62,8 @@ export function initTransactionGroupsApi(core: InternalCoreSetup) { }, tags: ['access:apm'] }, - handler: req => { - const setup = setupRequest(req); + handler: async req => { + const setup = await setupRequest(req); const { serviceName } = req.params; const { transactionType, transactionName } = req.query as { transactionType?: string; @@ -90,8 +93,8 @@ export function initTransactionGroupsApi(core: InternalCoreSetup) { }, tags: ['access:apm'] }, - handler: req => { - const setup = setupRequest(req); + handler: async req => { + const setup = await setupRequest(req); const { serviceName } = req.params; const { transactionType, @@ -127,8 +130,8 @@ export function initTransactionGroupsApi(core: InternalCoreSetup) { }) } }, - handler: req => { - const setup = setupRequest(req); + handler: async req => { + const setup = await setupRequest(req); const { serviceName } = req.params; const { transactionName, transactionType } = req.query as { transactionName?: string; diff --git a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts index 72c9e72286c0..9f6903c9840a 100644 --- a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts @@ -30,8 +30,8 @@ export function initUIFiltersApi(core: InternalCoreSetup) { }, tags: ['access:apm'] }, - handler: req => { - const setup = setupRequest(req); + handler: async req => { + const setup = await setupRequest(req); const { serviceName } = req.query as { serviceName?: string; }; diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts index 9b6aa0fa957a..f424ea21ab34 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts @@ -6,6 +6,12 @@ import { StringMap, IndexAsString } from './common'; +export interface BoolQuery { + must_not: Array>; + should: Array>; + filter: Array>; +} + declare module 'elasticsearch' { // extending SearchResponse to be able to have typed aggregations @@ -87,16 +93,16 @@ declare module 'elasticsearch' { }; extended_stats: { count: number; - min: number; - max: number; - avg: number; + min: number | null; + max: number | null; + avg: number | null; sum: number; - sum_of_squares: number; - variance: number; - std_deviation: number; + sum_of_squares: number | null; + variance: number | null; + std_deviation: number | null; std_deviation_bounds: { - upper: number; - lower: number; + upper: number | null; + lower: number | null; }; }; }[AggregationType & keyof AggregationOption[AggregationName]]; diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/ErrorRaw.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/ErrorRaw.ts index 8b917c7f5f34..2388556348f7 100644 --- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/ErrorRaw.ts +++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/ErrorRaw.ts @@ -6,10 +6,10 @@ import { APMBaseDoc } from './APMBaseDoc'; import { Container } from './fields/Container'; -import { Context } from './fields/Context'; import { Host } from './fields/Host'; import { Http } from './fields/Http'; 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'; @@ -47,12 +47,12 @@ export interface ErrorRaw extends APMBaseDoc { grouping_key: string; // either exception or log are given exception?: Exception[]; + page?: Page; // special property for RUM: shared by error and transaction log?: Log; }; // Shared by errors and transactions container?: Container; - context?: Context; host?: Host; http?: Http; kubernetes?: Kubernetes; diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts index 735efe73aed1..e72870f2197a 100644 --- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts +++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts @@ -6,10 +6,10 @@ import { APMBaseDoc } from './APMBaseDoc'; import { Container } from './fields/Container'; -import { Context } from './fields/Context'; import { Host } from './fields/Host'; import { Http } from './fields/Http'; import { Kubernetes } from './fields/Kubernetes'; +import { Page } from './fields/Page'; import { Process } from './fields/Process'; import { Service } from './fields/Service'; import { Url } from './fields/Url'; @@ -34,6 +34,7 @@ export interface TransactionRaw extends APMBaseDoc { }; }; name?: string; + page?: Page; // special property for RUM: shared by error and transaction result?: string; sampled: boolean; span_count?: { @@ -45,7 +46,6 @@ export interface TransactionRaw extends APMBaseDoc { // Shared by errors and transactions container?: Container; - context?: Context; host?: Host; http?: Http; kubernetes?: Kubernetes; diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Context.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Context.ts deleted file mode 100644 index eed626865868..000000000000 --- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Context.ts +++ /dev/null @@ -1,9 +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 interface Context { - page?: { url: string }; // only for RUM agent -} diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Page.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Page.ts new file mode 100644 index 000000000000..02498dd731de --- /dev/null +++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Page.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. + */ + +// only for RUM agent: shared by error and transaction +export interface Page { + url: string; +} diff --git a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js index f9ce4ad89c08..b6c8d95b05b2 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js +++ b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js @@ -15,6 +15,10 @@ import { addSerializer } from 'jest-specific-snapshot'; // Set our default timezone to UTC for tests so we can generate predictable snapshots moment.tz.setDefault('UTC'); +// Freeze time for the tests for predictable snapshots +const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 +Date.now = jest.fn(() => testTime); + // Mock EUI generated ids to be consistently predictable for snapshots. jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); @@ -31,22 +35,12 @@ jest.mock('../canvas_plugin_src/renderers/shape/shapes', () => ({ }, })); -// Mock datetime parsing so we can get stable results for tests (even while using the `now` format) -jest.mock('@elastic/datemath', () => { - return { - parse: (d, opts) => { - const dateMath = jest.requireActual('@elastic/datemath'); - return dateMath.parse(d, {...opts, forceNow: new Date(Date.UTC(2019, 5, 1))}); // June 1 2019 - } - } -}); - // Mock react-datepicker dep used by eui to avoid rendering the entire large component jest.mock('@elastic/eui/packages/react-datepicker', () => { return { __esModule: true, default: 'ReactDatePicker', - } + }; }); addSerializer(styleSheetSerializer); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index acbd1b7c6b1a..def16f2a4b23 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -6,6 +6,7 @@ import { openSans } from '../../../common/lib/fonts'; import header from './header.png'; +import { AdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; import { ElementFactory } from '../../../types'; export const metric: ElementFactory = () => ({ @@ -22,5 +23,6 @@ export const metric: ElementFactory = () => ({ | metric "Countries" metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} + metricFormat="${AdvancedSettings.get('format:number:defaultPattern')}" | render`, }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js index bed30182a2a2..50a952d83625 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js @@ -38,7 +38,7 @@ describe('metric', () => { }); }); - describe('metricStyle', () => { + describe('metricFont', () => { it('sets the font style for the metric', () => { const result = fn(null, { metricFont: fontStyle, @@ -51,7 +51,7 @@ describe('metric', () => { // it("sets a default style for the metric when not provided, () => {}); }); - describe('labelStyle', () => { + describe('labelFont', () => { it('sets the font style for the label', () => { const result = fn(null, { labelFont: fontStyle, @@ -63,5 +63,15 @@ describe('metric', () => { // TODO: write test when using an instance of the interpreter // it("sets a default style for the label when not provided, () => {}); }); + + describe('metricFormat', () => { + it('sets the number format of the metric value', () => { + const result = fn(null, { + metricFormat: '0.0%', + }); + + expect(result.value).toHaveProperty('metricFormat', '0.0%'); + }); + }); }); }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts index f27749dbe9fc..02d212f12fde 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts @@ -14,6 +14,7 @@ type Context = number | string | null; interface Arguments { label: string; metricFont: Style; + metricFormat: string; labelFont: Style; } @@ -35,26 +36,32 @@ export function metric(): ExpressionFunction<'metric', Context, Arguments, Rende help: argHelp.label, default: '""', }, + labelFont: { + types: ['style'], + help: argHelp.labelFont, + default: `{font size=14 family="${openSans.value}" color="#000000" align=center}`, + }, metricFont: { types: ['style'], help: argHelp.metricFont, default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, }, - labelFont: { - types: ['style'], - help: argHelp.labelFont, - default: `{font size=14 family="${openSans.value}" color="#000000" align=center}`, + metricFormat: { + types: ['string'], + aliases: ['format'], + help: argHelp.metricFormat, }, }, - fn: (context, { label, metricFont, labelFont }) => { + fn: (context, { label, labelFont, metricFont, metricFormat }) => { return { type: 'render', as: 'metric', value: { metric: context === null ? '?' : context, label, - metricFont, labelFont, + metricFont, + metricFormat, }, }; }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.examples.storyshot new file mode 100644 index 000000000000..22b235bcd797 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.examples.storyshot @@ -0,0 +1,263 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/Metric with formatted string metric and a specified format 1`] = ` +
    +
    +
    + $10m +
    +
    + Total Revenue +
    +
    +
    +`; + +exports[`Storyshots renderers/Metric with invalid metricFont 1`] = ` +
    +
    +
    + $10m +
    +
    + Total Revenue +
    +
    +
    +`; + +exports[`Storyshots renderers/Metric with label 1`] = ` +
    +
    +
    + $12.34 +
    +
    + Average price +
    +
    +
    +`; + +exports[`Storyshots renderers/Metric with null metric 1`] = ` +
    +
    +
    +
    +
    +`; + +exports[`Storyshots renderers/Metric with number metric 1`] = ` +
    +
    +
    + 12345.6789 +
    +
    +
    +`; + +exports[`Storyshots renderers/Metric with number metric and a specified format 1`] = ` +
    +
    +
    + -0.24% +
    +
    +
    +`; + +exports[`Storyshots renderers/Metric with string metric 1`] = ` +
    +
    +
    + $12.34 +
    +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.examples.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.examples.tsx new file mode 100644 index 000000000000..3ab6363ea7dd --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.examples.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { storiesOf } from '@storybook/react'; +import React, { CSSProperties } from 'react'; +import { Metric } from '../metric'; + +const labelFontSpec: CSSProperties = { + fontFamily: "Baskerville, Georgia, Garamond, 'Times New Roman', Times, serif", + fontWeight: 'normal', + fontStyle: 'italic', + textDecoration: 'none', + textAlign: 'center', + fontSize: '24px', + lineHeight: '1', + color: '#000000', +}; + +const metricFontSpec: CSSProperties = { + fontFamily: + "Optima, 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif", + fontWeight: 'bold', + fontStyle: 'normal', + textDecoration: 'none', + textAlign: 'center', + fontSize: '48px', + lineHeight: '1', + color: '#b83c6f', +}; + +storiesOf('renderers/Metric', module) + .addDecorator(story => ( +
    + {story()} +
    + )) + .add('with null metric', () => ) + .add('with number metric', () => ( + + )) + .add('with string metric', () => ( + + )) + .add('with label', () => ( + + )) + .add('with number metric and a specified format', () => ( + + )) + .add('with formatted string metric and a specified format', () => ( + + )) + .add('with invalid metricFont', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/index.ts new file mode 100644 index 000000000000..d830090249d5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/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 { Metric } from './metric'; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/metric.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/metric.tsx new file mode 100644 index 000000000000..8d2df8bf2233 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/metric.tsx @@ -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 React, { FunctionComponent, CSSProperties } from 'react'; +import numeral from '@elastic/numeral'; + +interface Props { + /** The text to display under the metric */ + label?: string; + /** CSS font properties for the label */ + labelFont: CSSProperties; + /** Value of the metric to display */ + metric: string | number | null; + /** CSS font properties for the metric */ + metricFont: CSSProperties; + /** NumeralJS format string */ + metricFormat?: string; +} + +export const Metric: FunctionComponent = ({ + label, + metric, + labelFont, + metricFont, + metricFormat, +}) => ( +
    +
    + {metricFormat ? numeral(metric).format(metricFormat) : metric} +
    + {label && ( +
    + {label} +
    + )} +
    +); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.js deleted file mode 100644 index 688b183f9a61..000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.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 ReactDOM from 'react-dom'; - -export const metric = () => ({ - name: 'metric', - displayName: 'Metric', - help: 'Render HTML Markup for the Metric element', - reuseDomNode: true, - render(domNode, config, handlers) { - const metricFontStyle = config.metricFont ? config.metricFont.spec : {}; - const labelFontStyle = config.labelFont ? config.labelFont.spec : {}; - - ReactDOM.render( -
    -
    - {config.metric} -
    -
    - {config.label} -
    -
    , - domNode, - () => handlers.done() - ); - - handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); - }, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.tsx new file mode 100644 index 000000000000..dd4d36aa7229 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.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, { CSSProperties } from 'react'; +import ReactDOM from 'react-dom'; +import { RendererFactory, Style } from '../../../types'; +import { Metric } from './component/metric'; + +export interface Config { + /** The text to display under the metric */ + label: string; + /** Font settings for the label */ + labelFont: Style; + /** Value of the metric to display */ + metric: string | number | null; + /** Font settings for the metric */ + metricFont: Style; + /** NumeralJS format string */ + metricFormat: string; +} + +export const metric: RendererFactory = () => ({ + name: 'metric', + displayName: 'Metric', + help: 'Render HTML Markup for the Metric element', + reuseDomNode: true, + render(domNode, config, handlers) { + ReactDOM.render( + , + domNode, + () => handlers.done() + ); + + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); + }, +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/pretty_duration/__examples__/__snapshots__/pretty_duration.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/pretty_duration/__examples__/__snapshots__/pretty_duration.examples.storyshot index 9a761ea731ca..3730cfb5f4e5 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/pretty_duration/__examples__/__snapshots__/pretty_duration.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/pretty_duration/__examples__/__snapshots__/pretty_duration.examples.storyshot @@ -1,5 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Storyshots renderers/TimeFilter/components/PrettyDuration with absolute dates 1`] = ` + + ~ 5 months ago to ~ 4 months ago + +`; + exports[`Storyshots renderers/TimeFilter/components/PrettyDuration with relative dates 1`] = ` Last 7 days diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/pretty_duration/__examples__/pretty_duration.examples.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/pretty_duration/__examples__/pretty_duration.examples.tsx index 3e6126320041..951776f8a955 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/pretty_duration/__examples__/pretty_duration.examples.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/pretty_duration/__examples__/pretty_duration.examples.tsx @@ -8,13 +8,6 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { PrettyDuration } from '..'; -storiesOf('renderers/TimeFilter/components/PrettyDuration', module).add( - 'with relative dates', - () => -); - -/** - * Disabling this test due to https://github.com/elastic/kibana/issues/41217 - * Re-enable when we have a better solution for mocking time used in format_duration - */ -// .add('with absolute dates', () => ); +storiesOf('renderers/TimeFilter/components/PrettyDuration', module) + .add('with relative dates', () => ) + .add('with absolute dates', () => ); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker_popover/__examples__/__snapshots__/time_picker_popover.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker_popover/__examples__/__snapshots__/time_picker_popover.examples.storyshot new file mode 100644 index 000000000000..030386122c0c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker_popover/__examples__/__snapshots__/time_picker_popover.examples.storyshot @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/TimeFilter/components/TimePickerPopover default 1`] = ` +
    +
    + +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker_popover/__examples__/time_picker_popover.examples.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker_popover/__examples__/time_picker_popover.examples.tsx index e6b6a1db1b42..7555de1336b3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker_popover/__examples__/time_picker_popover.examples.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker_popover/__examples__/time_picker_popover.examples.tsx @@ -4,20 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -/** - * Disabling this test due to https://github.com/elastic/kibana/issues/41217 - * Re-enable when we have a better solution for mocking time used in format_duration - */ - -// import { action } from '@storybook/addon-actions'; -// import { storiesOf } from '@storybook/react'; -// import moment from 'moment'; -// import React from 'react'; -// import { TimePickerPopover } from '..'; +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import moment from 'moment'; +import React from 'react'; +import { TimePickerPopover } from '..'; -// const startDate = moment.utc('2019-05-04').toISOString(); -// const endDate = moment.utc('2019-06-04').toISOString(); +const startDate = moment.utc('2019-05-04').toISOString(); +const endDate = moment.utc('2019-06-04').toISOString(); -// storiesOf('renderers/TimeFilter/components/TimePickerPopover', module).add('default', () => ( -// -// )); +storiesOf('renderers/TimeFilter/components/TimePickerPopover', module).add('default', () => ( + +)); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts index 8e92cd9efa9b..77bf71800d38 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { metric } from '../../functions/common/metric'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { FONT_FAMILY, FONT_WEIGHT, CSS } from '../constants'; +import { FONT_FAMILY, FONT_WEIGHT, CSS, NUMERALJS } from '../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.metricHelpText', { @@ -18,23 +18,33 @@ export const help: FunctionHelp> = { label: i18n.translate('xpack.canvas.functions.metric.args.labelHelpText', { defaultMessage: 'The text describing the metric.', }), - metricFont: i18n.translate('xpack.canvas.functions.metric.args.metricFontHelpText', { + labelFont: i18n.translate('xpack.canvas.functions.metric.args.labelFontHelpText', { defaultMessage: - 'The {CSS} font properties for the metric. For example, {FONT_FAMILY} or {FONT_WEIGHT}.', + 'The {CSS} font properties for the label. For example, {FONT_FAMILY} or {FONT_WEIGHT}.', values: { CSS, FONT_FAMILY, FONT_WEIGHT, }, }), - labelFont: i18n.translate('xpack.canvas.functions.metric.args.labelFontHelpText', { + metricFont: i18n.translate('xpack.canvas.functions.metric.args.metricFontHelpText', { defaultMessage: - 'The {CSS} font properties for the label. For example, {FONT_FAMILY} or {FONT_WEIGHT}.', + 'The {CSS} font properties for the metric. For example, {FONT_FAMILY} or {FONT_WEIGHT}.', values: { CSS, FONT_FAMILY, FONT_WEIGHT, }, }), + metricFormat: i18n.translate('xpack.canvas.functions.metric.args.metricFormatHelpText', { + defaultMessage: + 'A {NUMERALJS} format string. For example, {example1} or {example2}. See {url}.', + values: { + example1: `"0.0a"`, + example2: `"0%"`, + NUMERALJS, + url: 'http://numeraljs.com/#format', + }, + }), }, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/dashboard_report.json b/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/dashboard_report.json deleted file mode 100644 index 31c0d2076ea5..000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/dashboard_report.json +++ /dev/null @@ -1,455 +0,0 @@ -{ - "name": "Dashboard", - "id": "workpad-6181471b-147d-4397-a0d3-1c0f1600fa12", - "displayName": "Dashboard", - "help": "Infographic-style dashboard with live charts", - "tags": ["report"], - "width": 1100, - "height": 2570, - "page": 0, - "pages": [ - { - "id": "page-28d2523e-aa4d-4134-8092-b849835b620f", - "style": { - "background": "#FFF" - }, - "transition": {}, - "elements": [ - { - "id": "element-7e937714-3a57-4d41-bcc7-859b2d2db497", - "position": { - "left": -1.375, - "top": -2.5, - "width": 1101.75, - "height": 115, - "angle": 0, - "parent": null - }, - "expression": "shape \"square\" fill=\"#69707D\" border=\"rgba(255,255,255,0)\" borderWidth=0 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" containerStyle={containerStyle}" - }, - { - "id": "element-8cbe96d4-f555-4891-8f23-ef6cd679d9cf", - "position": { - "left": 31.75, - "top": 1186, - "width": 1034.5, - "height": 421, - "angle": 0, - "parent": null - }, - "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" - }, - { - "id": "element-9c467f5e-3594-41db-8602-ec45e4f3fe8f", - "position": { - "left": 566.25, - "top": 1650, - "width": 500, - "height": 386, - "angle": 0, - "parent": null - }, - "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" - }, - { - "id": "element-a07f8a00-d3da-470c-aea1-b88407900ba5", - "position": { - "left": 30.75, - "top": 1650, - "width": 508.25, - "height": 386, - "angle": 0, - "parent": null - }, - "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" - }, - { - "id": "element-80c70a23-12d9-4282-a68e-5d98ceb5a31f", - "position": { - "left": 31.75, - "top": 2084.5, - "width": 1034.5, - "height": 413, - "angle": 0, - "parent": null - }, - "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" - }, - { - "id": "element-105a0788-e347-4fa0-afff-0a6b80633b80", - "position": { - "left": 31.75, - "top": 707, - "width": 1034.5, - "height": 437, - "angle": 0, - "parent": null - }, - "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" - }, - { - "id": "element-f1d3d480-8aba-48cb-b5f0-2f6a62e64f3a", - "position": { - "left": 566.25, - "top": 158, - "width": 500, - "height": 508.5, - "angle": 0, - "parent": null - }, - "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" - }, - { - "id": "element-58634438-d8c7-4368-8e41-640d858374c3", - "position": { - "left": 31.75, - "top": 158, - "width": 507.25, - "height": 508.5, - "angle": 0, - "parent": null - }, - "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" - }, - { - "id": "element-9f76c74a-28d9-4ceb-bd7d-b1b34999a11e", - "position": { - "left": 52, - "top": 178, - "width": 500, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"### Total cost by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-3b6345a5-16ea-4828-beec-425458e758a7", - "position": { - "left": 591.25, - "top": 240, - "width": 455, - "height": 403, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| pointseries x=\"size(project)\" y=\"project\" color=\"project\"\n| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=false seriesStyle={seriesStyle label=\"elasticsearch\" color=\"#882e72\"}\n seriesStyle={seriesStyle label=\"machine-learning\" color=\"#d6c1de\"}\n seriesStyle={seriesStyle label=\"apm\" color=\"#5289c7\"}\n seriesStyle={seriesStyle label=\"kibana\" color=\"#7bafde\"}\n seriesStyle={seriesStyle label=\"beats\" color=\"#b178a6\"}\n seriesStyle={seriesStyle label=\"logstash\" color=\"#1965b0\"}\n seriesStyle={seriesStyle label=\"x-pack\" color=\"#4eb265\"}\n seriesStyle={seriesStyle label=\"swiftype\" color=\"#90c987\"}\n| render \n css=\".flot-y-axis {\n left: 14px !important;\n}\n\n.flot-x-axis>div {\n top: 380px !important;\n}\"" - }, - { - "id": "element-bdfb3910-5f65-4c24-9bbe-e62feb9e5e11", - "position": { - "left": 585.75, - "top": 178, - "width": 378, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"### Number of projects by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-161aafca-ba71-43e1-b2a2-dab96a78d717", - "position": { - "left": 53, - "top": 211, - "width": 500, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"##### Global cost distribution\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-d0c43968-cdcd-4a25-980f-83d6f0adf68e", - "position": { - "left": 586, - "top": 211, - "width": 500, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"##### Project type distribution\n\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-ea1f3942-066f-4032-a9d0-125072d353d9", - "position": { - "left": 61.75, - "top": 793, - "width": 643, - "height": 300, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| pointseries x=\"project\" y=\"mean(percent_uptime)\" color=\"project\"\n| plot defaultStyle={seriesStyle bars=0.75} legend=false seriesStyle={seriesStyle label=\"elasticsearch\" color=\"#882e72\"}\n seriesStyle={seriesStyle label=\"machine-learning\" color=\"#d6c1de\"}\n seriesStyle={seriesStyle label=\"apm\" color=\"#5289c7\"}\n seriesStyle={seriesStyle label=\"logstash\" color=\"#1965b0\"}\n seriesStyle={seriesStyle label=\"x-pack\" color=\"#4eb265\"}\n seriesStyle={seriesStyle label=\"kibana\" color=\"#7bafde\"}\n seriesStyle={seriesStyle label=\"swiftype\" color=\"#90c987\"}\n seriesStyle={seriesStyle label=\"beats\" color=\"#b178a6\"}\n| render css=\".flot-x-axis>div {\n top: 258px !important;\n}\"" - }, - { - "id": "element-5a891ee6-5cb8-4b8a-9c01-302ed42e6a8f", - "position": { - "left": 53, - "top": 726, - "width": 500, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"### Average uptime\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-09713339-044e-4084-b4e4-553dbc939d8a", - "position": { - "left": 729, - "top": 757, - "width": 301, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"##### Global average uptime\n\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-bd806eff-400b-4816-b728-b28a0390352d", - "position": { - "left": 764, - "top": 833.5, - "width": 200, - "height": 200, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| math \"mean(percent_uptime)\"\n| progress shape=\"wheel\" label={formatnumber \"0%\"} \n font={font size=24 family=\"'Open Sans', Helvetica, Arial, sans-serif\" color=\"#000000\" align=\"center\"} valueColor=\"#4eb265\"\n| render containerStyle={containerStyle}" - }, - { - "id": "element-ccd76ddc-2c03-458d-a0eb-09fcd1e2455f", - "position": { - "left": 53, - "top": 1212, - "width": 500, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"### Average price by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-ef88de44-1629-4a66-abc5-3764b03342e5", - "position": { - "left": 55.5, - "top": 2110, - "width": 500, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"### Raw data\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-1dbb5050-7b7c-4dd2-ab83-95913d15cc91", - "position": { - "left": 62.75, - "top": 273.75, - "width": 434.625, - "height": 285, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| pointseries color=\"project\" size=\"sum(cost)\"\n| pie hole=50 labels=false legend=\"ne\"\n| render \n css=\"table {\n right: -16px !important;\n}\n\n\ntr {\n height: 36px;\n}\n\n.legendColorBox div {\n margin-right: 7px;\n}\n\n.legendColorBox div div {\n width: 24px !important;\n height: 24px !important;\nborder-width: 4px !important;\n}\n\ntd {\n vertical-align: middle;\n}\" containerStyle={containerStyle overflow=\"visible\"}" - }, - { - "id": "element-8ca58ae7-2091-491f-996f-4256dfd5f4e1", - "position": { - "left": 51.875, - "top": 2162, - "width": 994.25, - "height": 300, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| table\n| render containerStyle={containerStyle overflow=\"hidden\"}" - }, - { - "id": "element-64db6690-dd39-4591-973d-d880e068de74", - "position": { - "left": 88, - "top": 1259.5, - "width": 902, - "height": 300, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| pointseries x=\"time\" y=\"mean(price)\" color=\"project\"\n| plot defaultStyle={seriesStyle lines=3} \n palette={palette \"#882E72\" \"#B178A6\" \"#D6C1DE\" \"#1965B0\" \"#5289C7\" \"#7BAFDE\" \"#4EB265\" \"#90C987\" \"#CAE0AB\" \"#F7EE55\" \"#F6C141\" \"#F1932D\" \"#E8601C\" \"#DC050C\" gradient=false} legend=\"ne\" seriesStyle={seriesStyle label=\"elasticsearch\" color=\"#882e72\"}\n seriesStyle={seriesStyle color=\"#b178a6\" label=\"beats\"}\n seriesStyle={seriesStyle label=\"machine-learning\" color=\"#d6c1de\"}\n seriesStyle={seriesStyle label=\"logstash\" color=\"#1965b0\"}\n seriesStyle={seriesStyle label=\"apm\" color=\"#5289c7\"}\n seriesStyle={seriesStyle label=\"kibana\" color=\"#7bafde\"}\n seriesStyle={seriesStyle label=\"x-pack\" color=\"#4eb265\"}\n seriesStyle={seriesStyle label=\"swiftype\" color=\"#90c987\"}\n| render containerStyle={containerStyle overflow=\"visible\"} \n css=\".legend table {\n top: 266px !important;\n width: 100%;\n left: 80px;\n}\n\n.legend td {\nvertical-align: middle;\n}\n\ntr {\n padding-left: 14px;\n}\n\n.legendLabel {\n padding-left: 4px;\n}\n\ntbody {\n display: flex;\n}\n\n.flot-x-axis {\n top: 16px !important;\n}\"" - }, - { - "id": "element-28fdc851-17bf-4a78-84f1-944fbf508d50", - "position": { - "left": 861.25, - "top": 44.75, - "width": 205, - "height": 36, - "angle": 0, - "parent": null - }, - "expression": "timefilterControl compact=true column=\"@timestamp\"\n| render css=\".canvasTimePickerPopover__button {\n border: none !important;\n}\"", - "filter": "timefilter from=\"now-14d\" to=now column=@timestamp" - }, - { - "id": "element-bf025bbc-7109-45a1-b954-bab851bc80df", - "position": { - "left": 764, - "top": 44.75, - "width": 89, - "height": 25, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"#### Time period\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#FFFFFF\" weight=\"normal\" underline=false italic=false}\n| render css=\"h4 {\n font-weight: 400;\n}\"" - }, - { - "id": "element-120f58cd-3ef0-40b6-99fd-32cc1480b9aa", - "position": { - "left": 53, - "top": 757, - "width": 500, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"##### Average uptime by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-c30023e3-5df6-4b54-8286-544811ce7b6a", - "position": { - "left": 51.875, - "top": 1670, - "width": 500, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"### Total cost by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-137409de-6f24-4234-9c5a-024054d0632a", - "position": { - "left": 593.25, - "top": 1665.5, - "width": 446, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"### Average price over time\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-b90b71f0-139b-419f-b43b-b2057abf777b", - "position": { - "left": 595.75, - "top": 1698.5, - "width": 223, - "height": 19, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"##### Price trend over time\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-a9b94f64-5336-4e39-ac69-5c9dacfbe129", - "position": { - "left": 53, - "top": 1703.5, - "width": 500, - "height": 38, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"##### State distribution\n\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" - }, - { - "id": "element-8777dd63-fbe7-446f-a23a-74cf55dc0a7c", - "position": { - "left": 109.75, - "top": 37.75, - "width": 500, - "height": 39, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| markdown \"## Monitoring Elastic projects\" \"\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#FFFFFF\" weight=\"bold\" underline=false italic=false}\n| render css=\".canvasRenderEl {\n\n}\"" - }, - { - "id": "element-5e85d913-fb4b-41d5-9caf-ca2de9970cc7", - "position": { - "left": 13.75, - "top": 29.8125, - "width": 92, - "height": 54.875, - "angle": 0, - "parent": null - }, - "expression": "image dataurl=null mode=\"contain\"\n| render" - }, - { - "id": "element-896f3043-4036-45f4-9e84-8aa6d870f215", - "position": { - "left": 53, - "top": 1729, - "width": 417.375, - "height": 290, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| pointseries x=\"sum(cost)\" y=\"project\" color=\"state\"\n| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=\"ne\"\n| render containerStyle={containerStyle overflow=\"visible\"} \n css=\".legend table {\n top: 100px !important;\n right: -46px !important;\n}\n\n.legendColorBox>div{\nmargin-right: 3px !important;\n}\n\n.legend td {\n\nvertical-align: middle;\n}\n\n.legend tr {\n height: 20px;\n}\n\n.flot-x-axis {\n top: -15px !important;\n}\n\n.flot-y-axis {\n left: 10px !important;\n}\"" - }, - { - "id": "element-13888369-9dac-4948-90b1-0ae42fa8fa53", - "position": { - "left": 593.75, - "top": 1733, - "width": 441, - "height": 282, - "angle": 0, - "parent": null - }, - "expression": "filters\n| demodata\n| pointseries x=\"time\" y=\"mean(price)\"\n| plot defaultStyle={seriesStyle bars=0.75} legend=false \n palette={palette \"#882E72\" \"#B178A6\" \"#D6C1DE\" \"#1965B0\" \"#5289C7\" \"#7BAFDE\" \"#4EB265\" \"#90C987\" \"#CAE0AB\" \"#F7EE55\" \"#F6C141\" \"#F1932D\" \"#E8601C\" \"#DC050C\" gradient=false}\n| render \n css=\".flot-x-axis {\n top: -15px !important;\n}\n\n.flot-y-axis {\n left: 10px !important;\n}\"" - } - ], - "groups": [] - } - ], - "colors": [ - "#37988d", - "#c19628", - "#b83c6f", - "#3f9939", - "#1785b0", - "#ca5f35", - "#45bdb0", - "#f2bc33", - "#e74b8b", - "#4fbf48", - "#1ea6dc", - "#fd7643", - "#72cec3", - "#f5cc5d", - "#ec77a8", - "#7acf74", - "#4cbce4", - "#fd986f", - "#a1ded7", - "#f8dd91", - "#f2a4c5", - "#a6dfa2", - "#86d2ed", - "#fdba9f", - "#000000", - "#444444", - "#777777", - "#BBBBBB", - "#FFFFFF", - "rgba(255,255,255,0)" - ], - "@timestamp": "2019-05-31T16:02:40.420Z", - "@created": "2019-05-31T16:01:45.751Z", - "assets": {}, - "css": "h3 {\ncolor: #343741;\nfont-weight: 400;\n}\n\nh5 {\ncolor: #69707D;\n}" -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.js index d551db843686..d67c47ed1b51 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.js @@ -8,7 +8,7 @@ const darkTemplate = require('./theme_dark.json'); const lightTemplate = require('./theme_light.json'); const pitchTemplate = require('./pitch_presentation.json'); const statusTemplate = require('./status_report.json'); -const dashboardTemplate = require('./dashboard_report.json'); +const summaryTemplate = require('./summary_report.json'); // Registry expects a function that returns a spec object export const templateSpecs = [ @@ -16,5 +16,5 @@ export const templateSpecs = [ lightTemplate, pitchTemplate, statusTemplate, - dashboardTemplate, + summaryTemplate, ].map(template => () => template); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/summary_report.json b/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/summary_report.json new file mode 100644 index 000000000000..6e4c2b2d71e9 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/summary_report.json @@ -0,0 +1,455 @@ +{ + "name": "Summary", + "id": "workpad-6181471b-147d-4397-a0d3-1c0f1600fa12", + "displayName": "Summary", + "help": "Infographic-style report with live charts", + "tags": ["report"], + "width": 1100, + "height": 2570, + "page": 0, + "pages": [ + { + "id": "page-28d2523e-aa4d-4134-8092-b849835b620f", + "style": { + "background": "#FFF" + }, + "transition": {}, + "elements": [ + { + "id": "element-7e937714-3a57-4d41-bcc7-859b2d2db497", + "position": { + "left": -1.375, + "top": -2.5, + "width": 1101.75, + "height": 115, + "angle": 0, + "parent": null + }, + "expression": "shape \"square\" fill=\"#69707D\" border=\"rgba(255,255,255,0)\" borderWidth=0 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" containerStyle={containerStyle}" + }, + { + "id": "element-8cbe96d4-f555-4891-8f23-ef6cd679d9cf", + "position": { + "left": 31.75, + "top": 1186, + "width": 1034.5, + "height": 421, + "angle": 0, + "parent": null + }, + "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" + }, + { + "id": "element-9c467f5e-3594-41db-8602-ec45e4f3fe8f", + "position": { + "left": 566.25, + "top": 1650, + "width": 500, + "height": 386, + "angle": 0, + "parent": null + }, + "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" + }, + { + "id": "element-a07f8a00-d3da-470c-aea1-b88407900ba5", + "position": { + "left": 30.75, + "top": 1650, + "width": 508.25, + "height": 386, + "angle": 0, + "parent": null + }, + "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" + }, + { + "id": "element-80c70a23-12d9-4282-a68e-5d98ceb5a31f", + "position": { + "left": 31.75, + "top": 2084.5, + "width": 1034.5, + "height": 413, + "angle": 0, + "parent": null + }, + "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" + }, + { + "id": "element-105a0788-e347-4fa0-afff-0a6b80633b80", + "position": { + "left": 31.75, + "top": 707, + "width": 1034.5, + "height": 437, + "angle": 0, + "parent": null + }, + "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" + }, + { + "id": "element-f1d3d480-8aba-48cb-b5f0-2f6a62e64f3a", + "position": { + "left": 566.25, + "top": 158, + "width": 500, + "height": 508.5, + "angle": 0, + "parent": null + }, + "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" + }, + { + "id": "element-58634438-d8c7-4368-8e41-640d858374c3", + "position": { + "left": 31.75, + "top": 158, + "width": 507.25, + "height": 508.5, + "angle": 0, + "parent": null + }, + "expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}" + }, + { + "id": "element-9f76c74a-28d9-4ceb-bd7d-b1b34999a11e", + "position": { + "left": 52, + "top": 178, + "width": 500, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"### Total cost by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-3b6345a5-16ea-4828-beec-425458e758a7", + "position": { + "left": 591.25, + "top": 240, + "width": 455, + "height": 403, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| pointseries x=\"size(project)\" y=\"project\" color=\"project\"\n| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=false seriesStyle={seriesStyle label=\"elasticsearch\" color=\"#882e72\"}\n seriesStyle={seriesStyle label=\"machine-learning\" color=\"#d6c1de\"}\n seriesStyle={seriesStyle label=\"apm\" color=\"#5289c7\"}\n seriesStyle={seriesStyle label=\"kibana\" color=\"#7bafde\"}\n seriesStyle={seriesStyle label=\"beats\" color=\"#b178a6\"}\n seriesStyle={seriesStyle label=\"logstash\" color=\"#1965b0\"}\n seriesStyle={seriesStyle label=\"x-pack\" color=\"#4eb265\"}\n seriesStyle={seriesStyle label=\"swiftype\" color=\"#90c987\"}\n| render \n css=\".flot-y-axis {\n left: 14px !important;\n}\n\n.flot-x-axis>div {\n top: 380px !important;\n}\"" + }, + { + "id": "element-bdfb3910-5f65-4c24-9bbe-e62feb9e5e11", + "position": { + "left": 585.75, + "top": 178, + "width": 378, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"### Number of projects by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-161aafca-ba71-43e1-b2a2-dab96a78d717", + "position": { + "left": 53, + "top": 211, + "width": 500, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"##### Global cost distribution\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-d0c43968-cdcd-4a25-980f-83d6f0adf68e", + "position": { + "left": 586, + "top": 211, + "width": 500, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"##### Project type distribution\n\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-ea1f3942-066f-4032-a9d0-125072d353d9", + "position": { + "left": 61.75, + "top": 793, + "width": 643, + "height": 300, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| pointseries x=\"project\" y=\"mean(percent_uptime)\" color=\"project\"\n| plot defaultStyle={seriesStyle bars=0.75} legend=false seriesStyle={seriesStyle label=\"elasticsearch\" color=\"#882e72\"}\n seriesStyle={seriesStyle label=\"machine-learning\" color=\"#d6c1de\"}\n seriesStyle={seriesStyle label=\"apm\" color=\"#5289c7\"}\n seriesStyle={seriesStyle label=\"logstash\" color=\"#1965b0\"}\n seriesStyle={seriesStyle label=\"x-pack\" color=\"#4eb265\"}\n seriesStyle={seriesStyle label=\"kibana\" color=\"#7bafde\"}\n seriesStyle={seriesStyle label=\"swiftype\" color=\"#90c987\"}\n seriesStyle={seriesStyle label=\"beats\" color=\"#b178a6\"}\n| render css=\".flot-x-axis>div {\n top: 258px !important;\n}\"" + }, + { + "id": "element-5a891ee6-5cb8-4b8a-9c01-302ed42e6a8f", + "position": { + "left": 53, + "top": 726, + "width": 500, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"### Average uptime\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-09713339-044e-4084-b4e4-553dbc939d8a", + "position": { + "left": 729, + "top": 757, + "width": 301, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"##### Global average uptime\n\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-bd806eff-400b-4816-b728-b28a0390352d", + "position": { + "left": 764, + "top": 833.5, + "width": 200, + "height": 200, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| math \"mean(percent_uptime)\"\n| progress shape=\"wheel\" label={formatnumber \"0%\"} \n font={font size=24 family=\"'Open Sans', Helvetica, Arial, sans-serif\" color=\"#000000\" align=\"center\"} valueColor=\"#4eb265\"\n| render containerStyle={containerStyle}" + }, + { + "id": "element-ccd76ddc-2c03-458d-a0eb-09fcd1e2455f", + "position": { + "left": 53, + "top": 1212, + "width": 500, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"### Average price by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-ef88de44-1629-4a66-abc5-3764b03342e5", + "position": { + "left": 55.5, + "top": 2110, + "width": 500, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"### Raw data\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-1dbb5050-7b7c-4dd2-ab83-95913d15cc91", + "position": { + "left": 62.75, + "top": 273.75, + "width": 434.625, + "height": 285, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| pointseries color=\"project\" size=\"sum(cost)\"\n| pie hole=50 labels=false legend=\"ne\"\n| render \n css=\"table {\n right: -16px !important;\n}\n\n\ntr {\n height: 36px;\n}\n\n.legendColorBox div {\n margin-right: 7px;\n}\n\n.legendColorBox div div {\n width: 24px !important;\n height: 24px !important;\nborder-width: 4px !important;\n}\n\ntd {\n vertical-align: middle;\n}\" containerStyle={containerStyle overflow=\"visible\"}" + }, + { + "id": "element-8ca58ae7-2091-491f-996f-4256dfd5f4e1", + "position": { + "left": 51.875, + "top": 2162, + "width": 994.25, + "height": 300, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| table\n| render containerStyle={containerStyle overflow=\"hidden\"}" + }, + { + "id": "element-64db6690-dd39-4591-973d-d880e068de74", + "position": { + "left": 88, + "top": 1259.5, + "width": 902, + "height": 300, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| pointseries x=\"time\" y=\"mean(price)\" color=\"project\"\n| plot defaultStyle={seriesStyle lines=3} \n palette={palette \"#882E72\" \"#B178A6\" \"#D6C1DE\" \"#1965B0\" \"#5289C7\" \"#7BAFDE\" \"#4EB265\" \"#90C987\" \"#CAE0AB\" \"#F7EE55\" \"#F6C141\" \"#F1932D\" \"#E8601C\" \"#DC050C\" gradient=false} legend=\"ne\" seriesStyle={seriesStyle label=\"elasticsearch\" color=\"#882e72\"}\n seriesStyle={seriesStyle color=\"#b178a6\" label=\"beats\"}\n seriesStyle={seriesStyle label=\"machine-learning\" color=\"#d6c1de\"}\n seriesStyle={seriesStyle label=\"logstash\" color=\"#1965b0\"}\n seriesStyle={seriesStyle label=\"apm\" color=\"#5289c7\"}\n seriesStyle={seriesStyle label=\"kibana\" color=\"#7bafde\"}\n seriesStyle={seriesStyle label=\"x-pack\" color=\"#4eb265\"}\n seriesStyle={seriesStyle label=\"swiftype\" color=\"#90c987\"}\n| render containerStyle={containerStyle overflow=\"visible\"} \n css=\".legend table {\n top: 266px !important;\n width: 100%;\n left: 80px;\n}\n\n.legend td {\nvertical-align: middle;\n}\n\ntr {\n padding-left: 14px;\n}\n\n.legendLabel {\n padding-left: 4px;\n}\n\ntbody {\n display: flex;\n}\n\n.flot-x-axis {\n top: 16px !important;\n}\"" + }, + { + "id": "element-28fdc851-17bf-4a78-84f1-944fbf508d50", + "position": { + "left": 861.25, + "top": 44.75, + "width": 205, + "height": 36, + "angle": 0, + "parent": null + }, + "expression": "timefilterControl compact=true column=\"@timestamp\"\n| render css=\".canvasTimePickerPopover__button {\n border: none !important;\n}\"", + "filter": "timefilter from=\"now-14d\" to=now column=@timestamp" + }, + { + "id": "element-bf025bbc-7109-45a1-b954-bab851bc80df", + "position": { + "left": 764, + "top": 44.75, + "width": 89, + "height": 25, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"#### Time period\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#FFFFFF\" weight=\"normal\" underline=false italic=false}\n| render css=\"h4 {\n font-weight: 400;\n}\"" + }, + { + "id": "element-120f58cd-3ef0-40b6-99fd-32cc1480b9aa", + "position": { + "left": 53, + "top": 757, + "width": 500, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"##### Average uptime by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-c30023e3-5df6-4b54-8286-544811ce7b6a", + "position": { + "left": 51.875, + "top": 1670, + "width": 500, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"### Total cost by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-137409de-6f24-4234-9c5a-024054d0632a", + "position": { + "left": 593.25, + "top": 1665.5, + "width": 446, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"### Average price over time\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-b90b71f0-139b-419f-b43b-b2057abf777b", + "position": { + "left": 595.75, + "top": 1698.5, + "width": 223, + "height": 19, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"##### Price trend over time\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-a9b94f64-5336-4e39-ac69-5c9dacfbe129", + "position": { + "left": 53, + "top": 1703.5, + "width": 500, + "height": 38, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"##### State distribution\n\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\"" + }, + { + "id": "element-8777dd63-fbe7-446f-a23a-74cf55dc0a7c", + "position": { + "left": 109.75, + "top": 37.75, + "width": 500, + "height": 39, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| markdown \"## Monitoring Elastic projects\" \"\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#FFFFFF\" weight=\"bold\" underline=false italic=false}\n| render css=\".canvasRenderEl {\n\n}\"" + }, + { + "id": "element-5e85d913-fb4b-41d5-9caf-ca2de9970cc7", + "position": { + "left": 13.75, + "top": 29.8125, + "width": 92, + "height": 54.875, + "angle": 0, + "parent": null + }, + "expression": "image dataurl=null mode=\"contain\"\n| render" + }, + { + "id": "element-896f3043-4036-45f4-9e84-8aa6d870f215", + "position": { + "left": 53, + "top": 1729, + "width": 417.375, + "height": 290, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| pointseries x=\"sum(cost)\" y=\"project\" color=\"state\"\n| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=\"ne\"\n| render containerStyle={containerStyle overflow=\"visible\"} \n css=\".legend table {\n top: 100px !important;\n right: -46px !important;\n}\n\n.legendColorBox>div{\nmargin-right: 3px !important;\n}\n\n.legend td {\n\nvertical-align: middle;\n}\n\n.legend tr {\n height: 20px;\n}\n\n.flot-x-axis {\n top: -15px !important;\n}\n\n.flot-y-axis {\n left: 10px !important;\n}\"" + }, + { + "id": "element-13888369-9dac-4948-90b1-0ae42fa8fa53", + "position": { + "left": 593.75, + "top": 1733, + "width": 441, + "height": 282, + "angle": 0, + "parent": null + }, + "expression": "filters\n| demodata\n| pointseries x=\"time\" y=\"mean(price)\"\n| plot defaultStyle={seriesStyle bars=0.75} legend=false \n palette={palette \"#882E72\" \"#B178A6\" \"#D6C1DE\" \"#1965B0\" \"#5289C7\" \"#7BAFDE\" \"#4EB265\" \"#90C987\" \"#CAE0AB\" \"#F7EE55\" \"#F6C141\" \"#F1932D\" \"#E8601C\" \"#DC050C\" gradient=false}\n| render \n css=\".flot-x-axis {\n top: -15px !important;\n}\n\n.flot-y-axis {\n left: 10px !important;\n}\"" + } + ], + "groups": [] + } + ], + "colors": [ + "#37988d", + "#c19628", + "#b83c6f", + "#3f9939", + "#1785b0", + "#ca5f35", + "#45bdb0", + "#f2bc33", + "#e74b8b", + "#4fbf48", + "#1ea6dc", + "#fd7643", + "#72cec3", + "#f5cc5d", + "#ec77a8", + "#7acf74", + "#4cbce4", + "#fd986f", + "#a1ded7", + "#f8dd91", + "#f2a4c5", + "#a6dfa2", + "#86d2ed", + "#fdba9f", + "#000000", + "#444444", + "#777777", + "#BBBBBB", + "#FFFFFF", + "rgba(255,255,255,0)" + ], + "@timestamp": "2019-05-31T16:02:40.420Z", + "@created": "2019-05-31T16:01:45.751Z", + "assets": {}, + "css": "h3 {\ncolor: #343741;\nfont-weight: 400;\n}\n\nh5 {\ncolor: #69707D;\n}" +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot new file mode 100644 index 000000000000..c256ff8e2350 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots arguments/AxisConfig extended 1`] = ` +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +`; + +exports[`Storyshots arguments/AxisConfig/components extended 1`] = ` +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +`; + +exports[`Storyshots arguments/AxisConfig/components extended disabled 1`] = ` +
    +
    +
    + The axis is disabled +
    +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.examples.storyshot new file mode 100644 index 000000000000..ccf2083a9c65 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.examples.storyshot @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots arguments/AxisConfig simple 1`] = ` +
    +
    + + + + + + + + +
    +
    +`; + +exports[`Storyshots arguments/AxisConfig/components simple template 1`] = ` +
    +
    + + + + + + + + +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.examples.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.examples.tsx new file mode 100644 index 000000000000..55f58efa37bf --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/extended_template.examples.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 { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { ExpressionAST } from '../../../../../types'; + +import { ExtendedTemplate } from '../extended_template'; + +const defaultExpression: ExpressionAST = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'axisConfig', + arguments: {}, + }, + ], +}; + +const defaultValues = { + argValue: defaultExpression, +}; + +class Interactive extends React.Component<{}, typeof defaultValues> { + public state = defaultValues; + + _onValueChange: (argValue: ExpressionAST) => void = argValue => { + action('onValueChange')(argValue); + this.setState({ argValue }); + }; + + public render() { + return ( + + ); + } +} + +storiesOf('arguments/AxisConfig', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('extended', () => ); + +storiesOf('arguments/AxisConfig/components', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('extended disabled', () => ( + + )) + .add('extended', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/simple_template.examples.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/simple_template.examples.tsx new file mode 100644 index 000000000000..1446fe2933f8 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/simple_template.examples.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 { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; + +import { SimpleTemplate } from '../simple_template'; + +const defaultValues = { + argValue: false, +}; + +class Interactive extends React.Component<{}, typeof defaultValues> { + public state = defaultValues; + + public render() { + return ( + { + action('onValueChange')(argValue); + this.setState({ argValue }); + }} + argValue={this.state.argValue} + /> + ); + } +} + +storiesOf('arguments/AxisConfig', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('simple', () => ); + +storiesOf('arguments/AxisConfig/components', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('simple template', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.js deleted file mode 100644 index 1f7b09c66ae6..000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.js +++ /dev/null @@ -1,77 +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, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { EuiSelect, EuiFormRow, EuiText } from '@elastic/eui'; -import { set } from 'object-path-immutable'; -import { get } from 'lodash'; - -const defaultExpression = { - type: 'expression', - chain: [ - { - type: 'function', - function: 'axisConfig', - arguments: {}, - }, - ], -}; - -export class ExtendedTemplate extends React.PureComponent { - static propTypes = { - onValueChange: PropTypes.func.isRequired, - argValue: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - chain: PropTypes.array, - }).isRequired, - ]), - typeInstance: PropTypes.object.isRequired, - argId: PropTypes.string.isRequired, - }; - - // TODO: this should be in a helper, it's the same code from container_style - getArgValue = (name, alt) => { - return get(this.props.argValue, ['chain', 0, 'arguments', name, 0], alt); - }; - - // TODO: this should be in a helper, it's the same code from container_style - setArgValue = name => ev => { - const val = ev.target.value; - const { argValue, onValueChange } = this.props; - const oldVal = typeof argValue === 'boolean' ? defaultExpression : argValue; - const newValue = set(oldVal, ['chain', 0, 'arguments', name, 0], val); - onValueChange(newValue); - }; - - render() { - const isDisabled = typeof this.props.argValue === 'boolean' && this.props.argValue === false; - - if (isDisabled) { - return The axis is disabled; - } - - const positions = { - xaxis: ['bottom', 'top'], - yaxis: ['left', 'right'], - }; - const argName = this.props.typeInstance.name; - const position = this.getArgValue('position', positions[argName][0]); - - const options = positions[argName].map(val => ({ value: val, text: val })); - - return ( - - - - - - ); - } -} - -ExtendedTemplate.displayName = 'AxisConfigExtendedInput'; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx new file mode 100644 index 000000000000..92d38013855c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.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, { Fragment, ChangeEvent, PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiSelect, EuiFormRow, EuiText } from '@elastic/eui'; +import immutable from 'object-path-immutable'; +import { get } from 'lodash'; +import { ExpressionAST } from '../../../../types'; + +const { set } = immutable; + +const defaultExpression: ExpressionAST = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'axisConfig', + arguments: {}, + }, + ], +}; + +export interface Props { + onValueChange: (newValue: ExpressionAST) => void; + argValue: boolean | ExpressionAST; + typeInstance: { + name: 'xaxis' | 'yaxis'; + }; +} + +export class ExtendedTemplate extends PureComponent { + static propTypes = { + onValueChange: PropTypes.func.isRequired, + argValue: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.shape({ + chain: PropTypes.array, + }).isRequired, + ]), + typeInstance: PropTypes.object.isRequired, + }; + + static displayName = 'AxisConfigExtendedInput'; + + // TODO: this should be in a helper, it's the same code from container_style + getArgValue = (name: string, alt: string) => { + return get(this.props.argValue, `chain.0.arguments.${name}.0`, alt); + }; + + // TODO: this should be in a helper, it's the same code from container_style + setArgValue = (name: string) => (ev: ChangeEvent) => { + if (!ev || !ev.target) { + return; + } + + const val = ev.target.value; + const { argValue, onValueChange } = this.props; + const oldVal = typeof argValue === 'boolean' ? defaultExpression : argValue; + const newValue = set(oldVal, `chain.0.arguments.${name}.0`, val); + onValueChange(newValue); + }; + + render() { + const isDisabled = typeof this.props.argValue === 'boolean' && this.props.argValue === false; + + if (isDisabled) { + return The axis is disabled; + } + + const positions = { + xaxis: ['bottom', 'top'], + yaxis: ['left', 'right'], + }; + const argName = this.props.typeInstance.name; + const position = this.getArgValue('position', positions[argName][0]); + + const options = positions[argName].map(val => ({ value: val, text: val })); + + return ( + + + + + + ); + } +} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/index.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/index.js rename to x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/index.ts diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.js deleted file mode 100644 index 23bca236f450..000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.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. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiSwitch } from '@elastic/eui'; - -export const SimpleTemplate = ({ onValueChange, argValue }) => ( - onValueChange(!Boolean(argValue))} /> -); - -SimpleTemplate.propTypes = { - onValueChange: PropTypes.func.isRequired, - argValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]).isRequired, -}; - -SimpleTemplate.displayName = 'AxisConfigSimpleInput'; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.tsx new file mode 100644 index 000000000000..bd85d9555652 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.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 React, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiSwitch } from '@elastic/eui'; + +export interface Props { + onValueChange: (argValue: boolean) => void; + argValue: boolean; +} + +export const SimpleTemplate: FunctionComponent = ({ onValueChange, argValue }) => { + return ( + onValueChange(!Boolean(argValue))} /> + ); +}; + +SimpleTemplate.propTypes = { + onValueChange: PropTypes.func.isRequired, + argValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]).isRequired, +}; + +SimpleTemplate.displayName = 'AxisConfigSimpleInput'; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js index db13083c9738..b714cdcaab09 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js @@ -9,6 +9,7 @@ import { datacolumn } from './datacolumn'; import { filterGroup } from './filter_group'; import { imageUpload } from './image_upload'; import { number } from './number'; +import { numberFormat } from './number_format'; import { palette } from './palette'; import { percentage } from './percentage'; import { range } from './range'; @@ -24,6 +25,7 @@ export const args = [ filterGroup, imageUpload, number, + numberFormat, palette, percentage, range, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot new file mode 100644 index 000000000000..6fcfc922e5d0 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot @@ -0,0 +1,242 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots arguments/NumberFormat with custom format 1`] = ` +Array [ +
    +
    + +
    + + + +
    +
    +
    , +
    , +
    +
    + +
    +
    , +] +`; + +exports[`Storyshots arguments/NumberFormat with no format 1`] = ` +Array [ +
    +
    + +
    + + + +
    +
    +
    , +
    , +
    +
    + +
    +
    , +] +`; + +exports[`Storyshots arguments/NumberFormat with preset format 1`] = ` +
    +
    + +
    + + + +
    +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.examples.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.examples.tsx new file mode 100644 index 000000000000..4a078a38c4a3 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.examples.tsx @@ -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 { storiesOf } from '@storybook/react'; +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { NumberFormatArgInput } from '../number_format'; + +const numberFormats = [ + { value: '0.0[000]', text: 'Number' }, + { value: '0.0%', text: 'Percent' }, + { value: '$0.00', text: 'Currency' }, + { value: '00:00:00', text: 'Duration' }, + { value: '0.0b', text: 'Bytes' }, +]; + +storiesOf('arguments/NumberFormat', module) + .add('with no format', () => ( + + )) + .add('with preset format', () => ( + + )) + .add('with custom format', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts new file mode 100644 index 000000000000..4eca6e41bd3f --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/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 { compose, withProps } from 'recompose'; +import { NumberFormatArgInput as Component, Props as ComponentProps } from './number_format'; +import { AdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; +// @ts-ignore untyped local lib +import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; + +const formatMap = { + NUMBER: AdvancedSettings.get('format:number:defaultPattern'), + PERCENT: AdvancedSettings.get('format:percent:defaultPattern'), + CURRENCY: AdvancedSettings.get('format:currency:defaultPattern'), + DURATION: '00:00:00', + BYTES: AdvancedSettings.get('format:bytes:defaultPattern'), +}; + +const numberFormats = [ + { value: formatMap.NUMBER, text: 'Number' }, + { value: formatMap.PERCENT, text: 'Percent' }, + { value: formatMap.CURRENCY, text: 'Currency' }, + { value: formatMap.DURATION, text: 'Duration' }, + { value: formatMap.BYTES, text: 'Bytes' }, +]; + +export const NumberFormatArgInput = compose(withProps({ numberFormats }))( + Component +); + +export const numberFormat = () => ({ + name: 'numberFormat', + displayName: 'Number Format', + help: 'Select or enter a valid NumeralJS format', + simpleTemplate: templateFromReactComponent(NumberFormatArgInput), +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/number_format.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/number_format.tsx new file mode 100644 index 000000000000..b9d122dcf03d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/number_format.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, { Fragment, ChangeEvent, FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiSelect, EuiFieldText, EuiSpacer } from '@elastic/eui'; + +interface NumberFormatOption { + /** A NumeralJS format string */ + value: string; + /** The name to display for the format */ + text: string; +} + +export interface Props { + /** An array of number formats options */ + numberFormats: NumberFormatOption[]; + /** The handler to invoke when value changes */ + onValueChange: (value: string) => void; + /** The value of the argument */ + argValue: string; + /** The ID for the argument */ + argId: string; +} + +export const NumberFormatArgInput: FunctionComponent = ({ + numberFormats, + onValueChange, + argValue, + argId, +}) => { + const formatOptions = numberFormats.concat({ value: '', text: 'Custom' }); + const handleTextChange = (ev: ChangeEvent) => onValueChange(ev.target.value); + const handleSelectChange = (ev: ChangeEvent) => { + const { value } = formatOptions[ev.target.selectedIndex]; + return onValueChange(value || '0.0a'); + }; + + // checks if the argValue is one of the preset formats + const isCustomFormat = !argValue || !formatOptions.map(({ value }) => value).includes(argValue); + + return ( + + + {isCustomFormat && ( + + + + + )} + + ); +}; + +NumberFormatArgInput.propTypes = { + onValueChange: PropTypes.func.isRequired, + argValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).isRequired, + argId: PropTypes.string.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js index 15da536c56e4..5df400361511 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js @@ -5,6 +5,7 @@ */ import { openSans } from '../../../common/lib/fonts'; +import { AdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; export const metric = () => ({ name: 'metric', @@ -19,6 +20,13 @@ export const metric = () => ({ argType: 'string', default: '""', }, + { + name: 'labelFont', + displayName: 'Label text settings', + help: 'Fonts, alignment and color', + argType: 'font', + default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, + }, { name: 'metricFont', displayName: 'Metric text settings', @@ -27,11 +35,10 @@ export const metric = () => ({ default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, }, { - name: 'labelFont', - displayName: 'Label text settings', - help: 'Fonts, alignment and color', - argType: 'font', - default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, + name: 'metricFormat', + displayName: 'Metric Format', + argType: 'numberFormat', + default: `"${AdvancedSettings.get('format:number:defaultPattern')}"`, }, ], }); diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss index fb53225bdd38..a63d75ce9a7d 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss @@ -22,7 +22,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stage { flex-grow: 1; - flex-basis: 0%; + flex-basis: auto; display: flex; flex-direction: column; } @@ -43,7 +43,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageContent { flex-grow: 1; - flex-basis: 0%; + flex-basis: auto; position: relative; } diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot index 6cdca7109222..fb63022f341d 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot @@ -75,7 +75,6 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` onMouseOver={[Function]} > +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`Storyshots arguments/ContainerStyle/components appearance form 1`] = ` +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`Storyshots arguments/ContainerStyle/components border form 1`] = ` +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + + Select an option: +
    + , is selected + + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`Storyshots arguments/ContainerStyle/components extended template 1`] = ` +
    +
    +
    + Appearance +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + Border +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + + Select an option: +
    + , is selected + + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.examples.storyshot new file mode 100644 index 000000000000..f8d460a63421 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.examples.storyshot @@ -0,0 +1,125 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots arguments/ContainerStyle simple 1`] = ` +
    +
    +
    +
    + +
    +
    +
    +
    +`; + +exports[`Storyshots arguments/ContainerStyle/components simple template 1`] = ` +
    +
    +
    +
    + +
    +
    +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx new file mode 100644 index 000000000000..51a1608df67a --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { storiesOf } from '@storybook/react'; +import React from 'react'; +// @ts-ignore Untyped local +import { getDefaultWorkpad } from '../../../../state/defaults'; + +import { Arguments, ArgumentTypes, BorderStyle, ExtendedTemplate } from '../extended_template'; +import { BorderForm } from '../border_form'; +import { AppearanceForm } from '../appearance_form'; + +const defaultValues: Arguments = { + padding: 0, + opacity: 1, + overflow: 'visible', + borderRadius: 0, + borderStyle: BorderStyle.SOLID, + borderWidth: 1, + border: '1px solid #fff', +}; + +class Interactive extends React.Component<{}, Arguments> { + public state = defaultValues; + + _getArgValue: (arg: T) => Arguments[T] = arg => { + return this.state[arg]; + }; + + _setArgValue: (arg: T, val: ArgumentTypes[T]) => void = ( + arg, + val + ) => { + action('setArgValue')(arg, val); + this.setState({ ...this.state, [arg]: val }); + }; + + public render() { + return ( + + ); + } +} + +const getArgValue: (arg: T) => Arguments[T] = arg => { + return defaultValues[arg]; +}; + +storiesOf('arguments/ContainerStyle', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('extended', () => ); + +storiesOf('arguments/ContainerStyle/components', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('appearance form', () => ( + + )) + .add('border form', () => ( + + )) + .add('extended template', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx new file mode 100644 index 000000000000..71d95603cfeb --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx @@ -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 { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +// @ts-ignore Untyped local +import { getDefaultWorkpad } from '../../../../state/defaults'; + +import { Argument, Arguments, SimpleTemplate } from '../simple_template'; + +const defaultValues: Arguments = { + backgroundColor: '#fff', +}; + +class Interactive extends React.Component<{}, Arguments> { + public state = defaultValues; + + _getArgValue: (arg: T) => Arguments[T] = arg => { + return this.state[arg]; + }; + + _setArgValue: (arg: T, val: Arguments[T]) => void = (arg, val) => { + action('setArgValue')(arg, val); + this.setState({ ...this.state, [arg]: val }); + }; + + public render() { + return ( + + ); + } +} + +const getArgValue: (arg: T) => Arguments[T] = arg => { + return defaultValues[arg]; +}; + +storiesOf('arguments/ContainerStyle', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('simple', () => ); + +storiesOf('arguments/ContainerStyle/components', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('simple template', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/appearance_form.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/appearance_form.js deleted file mode 100644 index 5a8b3ce96a11..000000000000 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/appearance_form.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; - -const opacities = [ - { value: 1, text: '100%' }, - { value: 0.9, text: '90%' }, - { value: 0.7, text: '70%' }, - { value: 0.5, text: '50%' }, - { value: 0.3, text: '30%' }, - { value: 0.1, text: '10%' }, -]; - -const overflows = [{ value: 'hidden', text: 'Hidden' }, { value: 'visible', text: 'Visible' }]; - -export const AppearanceForm = ({ padding, opacity, overflow, onChange }) => { - const paddingVal = padding ? padding.replace('px', '') : ''; - - const namedChange = name => ev => { - if (name === 'padding') { - return onChange(name, `${ev.target.value}px`); - } - - onChange(name, ev.target.value); - }; - - return ( - - - - - - - - - - - - - - - - - - ); -}; - -AppearanceForm.propTypes = { - padding: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - opacity: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - overflow: PropTypes.oneOf(['hidden', 'visible']), - onChange: PropTypes.func.isRequired, -}; - -AppearanceForm.defaultProps = { - opacity: 1, - overflow: 'hidden', -}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/appearance_form.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/appearance_form.tsx new file mode 100644 index 000000000000..518c4a0256b2 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/appearance_form.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, ChangeEvent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; + +type Overflow = 'hidden' | 'visible'; + +export interface Arguments { + padding: string | number; + opacity: string | number; + overflow: Overflow; +} +export type ArgumentTypes = Arguments; +export type Argument = keyof Arguments; + +interface Props extends Arguments { + onChange: (arg: T, val: ArgumentTypes[T]) => void; +} + +const overflows: Array<{ value: Overflow; text: string }> = [ + { value: 'hidden', text: 'Hidden' }, + { value: 'visible', text: 'Visible' }, +]; + +const opacities = [ + { value: 1, text: '100%' }, + { value: 0.9, text: '90%' }, + { value: 0.7, text: '70%' }, + { value: 0.5, text: '50%' }, + { value: 0.3, text: '30%' }, + { value: 0.1, text: '10%' }, +]; + +export const AppearanceForm: FunctionComponent = ({ + padding = '', + opacity = 1, + overflow = 'hidden', + onChange, +}) => { + if (typeof padding === 'string') { + padding = padding.replace('px', ''); + } + + const namedChange = (name: keyof Arguments) => ( + ev: ChangeEvent + ) => { + if (name === 'padding') { + return onChange(name, `${ev.target.value}px`); + } + + onChange(name, ev.target.value); + }; + + return ( + + + + + + + + + + + + + + + + + + ); +}; + +AppearanceForm.propTypes = { + padding: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + opacity: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + overflow: PropTypes.oneOf(['hidden', 'visible']), + onChange: PropTypes.func.isRequired, +}; + +AppearanceForm.defaultProps = { + opacity: 1, + overflow: 'hidden', +}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/border_form.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/border_form.js deleted file mode 100644 index 00169c865458..000000000000 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/border_form.js +++ /dev/null @@ -1,107 +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 PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFormRow, - EuiFlexItem, - EuiFieldNumber, - EuiSuperSelect, -} from '@elastic/eui'; -import { ColorPickerPopover } from '../../../components/color_picker_popover'; - -const styles = [ - 'none', - 'solid', - 'dotted', - 'dashed', - 'double', - 'groove', - 'ridge', - 'inset', - 'outset', -]; - -export const BorderForm = ({ value, radius, onChange, colors }) => { - const border = value || ''; - const [borderWidth = '', borderStyle = '', borderColor = ''] = border.split(' '); - const borderWidthVal = borderWidth ? borderWidth.replace('px', '') : ''; - const radiusVal = radius ? radius.replace('px', '') : ''; - - const namedChange = name => val => { - if (name === 'borderWidth') { - return onChange('border', `${val}px ${borderStyle} ${borderColor}`); - } - if (name === 'borderStyle') { - if (val === '') { - return onChange('border', `0px`); - } - return onChange('border', `${borderWidth} ${val} ${borderColor}`); - } - if (name === 'borderRadius') { - return onChange('borderRadius', `${val}px`); - } - - onChange(name, val); - }; - - const borderColorChange = color => onChange('border', `${borderWidth} ${borderStyle} ${color}`); - - return ( - - - - namedChange('borderWidth')(e.target.value)} - /> - - - - - - ({ - value: style, - inputDisplay:
    , - }))} - onChange={namedChange('borderStyle')} - /> - - - - - - namedChange('borderRadius')(e.target.value)} - /> - - - - - - - - - - ); -}; - -BorderForm.propTypes = { - value: PropTypes.string, - radius: PropTypes.string, - onChange: PropTypes.func.isRequired, - colors: PropTypes.array.isRequired, -}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/border_form.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/border_form.tsx new file mode 100644 index 000000000000..55e2c6ecda93 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/border_form.tsx @@ -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 React, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiFlexGroup, + EuiFormRow, + EuiFlexItem, + EuiFieldNumber, + EuiSuperSelect, +} from '@elastic/eui'; +import { ColorPickerPopover } from '../../../components/color_picker_popover'; +import { BorderStyle, isBorderStyle } from '../../../../types'; + +export { BorderStyle } from '../../../../types'; + +export interface Arguments { + borderRadius: string | number; + borderStyle: BorderStyle; + borderWidth: number; + border: string; +} +export type ArgumentTypes = Arguments; +export type Argument = keyof Arguments; + +interface Props { + onChange: (arg: T, val: ArgumentTypes[T]) => void; + value: string; + radius: string | number; + colors: string[]; +} + +export const BorderForm: FunctionComponent = ({ + value = '', + radius = '', + onChange, + colors, +}) => { + const [borderWidth = '', borderStyle = '', borderColor = ''] = value.split(' '); + + const borderStyleVal = isBorderStyle(borderStyle) ? borderStyle : BorderStyle.NONE; + const borderWidthVal = borderWidth ? borderWidth.replace('px', '') : ''; + const radiusVal = typeof radius === 'string' ? radius.replace('px', '') : radius; + + const namedChange = (name: T) => (val: Arguments[T]) => { + if (name === 'borderWidth') { + return onChange('border', `${val}px ${borderStyle} ${borderColor}`); + } + if (name === 'borderStyle') { + if (val === '') { + return onChange('border', `0px`); + } + return onChange('border', `${borderWidth} ${val} ${borderColor}`); + } + if (name === 'borderRadius') { + if (val === '') { + return onChange('borderRadius', `0px`); + } + return onChange('borderRadius', `${val}px`); + } + + onChange(name, val); + }; + + const borderColorChange = (color: string) => + onChange('border', `${borderWidth} ${borderStyle} ${color}`); + + return ( + + + + namedChange('borderWidth')(Number(e.target.value))} + /> + + + + + + ({ + value: style, + inputDisplay:
    , + }))} + onChange={namedChange('borderStyle')} + /> + + + + + + namedChange('borderRadius')(e.target.value)} + /> + + + + + + + + + + ); +}; + +BorderForm.propTypes = { + value: PropTypes.string, + radius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + onChange: PropTypes.func.isRequired, + colors: PropTypes.array.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/extended_template.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/extended_template.js deleted file mode 100644 index bfe0522476f8..000000000000 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/extended_template.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiSpacer, EuiTitle } from '@elastic/eui'; -import { BorderForm } from './border_form'; -import { AppearanceForm } from './appearance_form'; - -export const ExtendedTemplate = ({ getArgValue, setArgValue, workpad }) => ( -
    - -
    Appearance
    -
    - - - - - - - -
    Border
    -
    - - - -
    -); - -ExtendedTemplate.displayName = 'ContainerStyleArgExtendedInput'; - -ExtendedTemplate.propTypes = { - onValueChange: PropTypes.func.isRequired, - argValue: PropTypes.any.isRequired, - getArgValue: PropTypes.func.isRequired, - setArgValue: PropTypes.func.isRequired, - workpad: PropTypes.shape({ - colors: PropTypes.array.isRequired, - }).isRequired, -}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/extended_template.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/extended_template.tsx new file mode 100644 index 000000000000..88890e551eae --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/extended_template.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, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import { BorderForm } from './border_form'; +import { AppearanceForm } from './appearance_form'; +import { CanvasWorkpad } from '../.../../../../../types'; +import { Arguments as AppearanceArguments } from './appearance_form'; +import { Arguments as BorderArguments } from './border_form'; + +export { BorderStyle } from './border_form'; + +export interface Arguments extends BorderArguments, AppearanceArguments {} +export type ArgumentTypes = Partial; +export type Argument = keyof ArgumentTypes; + +interface Props { + getArgValue: (arg: T) => Arguments[T]; + setArgValue: (arg: T, val: ArgumentTypes[T]) => void; + workpad: CanvasWorkpad; +} + +export const ExtendedTemplate: FunctionComponent = ({ + getArgValue, + setArgValue, + workpad, +}) => ( +
    + +
    Appearance
    +
    + + + + + +
    Border
    +
    + + + +
    +); + +ExtendedTemplate.displayName = 'ContainerStyleArgExtendedInput'; + +ExtendedTemplate.propTypes = { + getArgValue: PropTypes.func.isRequired, + setArgValue: PropTypes.func.isRequired, + workpad: PropTypes.shape({ + colors: PropTypes.array.isRequired, + }).isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/index.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/index.js deleted file mode 100644 index d2a3bf59f6b4..000000000000 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/index.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 { withHandlers } from 'recompose'; -import { set } from 'object-path-immutable'; -import { get } from 'lodash'; -import { templateFromReactComponent } from '../../../lib/template_from_react_component'; -import { SimpleTemplate } from './simple_template'; -import { ExtendedTemplate } from './extended_template'; - -const wrap = Component => - // TODO: this should be in a helper - withHandlers({ - getArgValue: ({ argValue }) => (name, alt) => { - const args = get(argValue, 'chain.0.arguments', {}); - return get(args, [name, 0], alt); - }, - setArgValue: ({ argValue, onValueChange }) => (name, val) => { - const newValue = set(argValue, ['chain', 0, 'arguments', name, 0], val); - onValueChange(newValue); - }, - })(Component); - -export const containerStyle = () => ({ - name: 'containerStyle', - displayName: 'Container style', - help: 'Tweak the appearance of the element container', - default: '{containerStyle}', - simpleTemplate: templateFromReactComponent(wrap(SimpleTemplate)), - template: templateFromReactComponent(wrap(ExtendedTemplate)), -}); diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/index.ts b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/index.ts new file mode 100644 index 000000000000..c465c3fa35c3 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/index.ts @@ -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 { ComponentType } from 'react'; +import { withHandlers } from 'recompose'; +import immutable from 'object-path-immutable'; +import { get } from 'lodash'; +import { templateFromReactComponent } from '../../../lib/template_from_react_component'; +import { Arguments as SimpleArguments, SimpleTemplate } from './simple_template'; +import { Arguments as ExtendedArguments, ExtendedTemplate } from './extended_template'; + +const { set } = immutable; + +interface Arguments extends SimpleArguments, ExtendedArguments {} +type ArgumentTypes = Partial; +type Argument = keyof ArgumentTypes; + +interface Handlers { + getArgValue: (name: T, alt: Arguments[T]) => Arguments[T]; + setArgValue: (name: T, val: ArgumentTypes[T]) => void; +} + +interface OuterProps { + argValue: keyof Arguments; + onValueChange: Function; +} + +const wrap = (Component: ComponentType) => + // TODO: this should be in a helper + withHandlers({ + getArgValue: ({ argValue }) => (name, alt) => { + const args = get(argValue, 'chain.0.arguments', {}); + return get(args, `${name}.0`, alt); + }, + setArgValue: ({ argValue, onValueChange }) => (name, val) => { + const newValue = set(argValue, `chain.0.arguments.${name}.0`, val); + onValueChange(newValue); + }, + })(Component); + +export const containerStyle = () => ({ + name: 'containerStyle', + displayName: 'Container style', + help: 'Tweak the appearance of the element container', + default: '{containerStyle}', + simpleTemplate: templateFromReactComponent(wrap(SimpleTemplate)), + template: templateFromReactComponent(wrap(ExtendedTemplate)), +}); diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.js deleted file mode 100644 index 96a29b220a3c..000000000000 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { ColorPickerPopover } from '../../../components/color_picker_popover'; - -export const SimpleTemplate = ({ getArgValue, setArgValue, workpad }) => ( -
    - setArgValue('backgroundColor', color)} - colors={workpad.colors} - anchorPosition="leftCenter" - /> -
    -); - -SimpleTemplate.displayName = 'ContainerStyleArgSimpleInput'; - -SimpleTemplate.propTypes = { - onValueChange: PropTypes.func.isRequired, - argValue: PropTypes.any.isRequired, - getArgValue: PropTypes.func.isRequired, - setArgValue: PropTypes.func.isRequired, - workpad: PropTypes.shape({ - colors: PropTypes.array.isRequired, - }), -}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.tsx new file mode 100644 index 000000000000..11e000e08481 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/simple_template.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, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { ColorPickerPopover } from '../../../components/color_picker_popover'; +import { CanvasWorkpad } from '../.../../../../../types'; + +export interface Arguments { + backgroundColor: string; +} +export type Argument = keyof Arguments; + +interface Props { + getArgValue: (key: T) => Arguments[T]; + setArgValue: (key: T, val: Arguments[T]) => void; + workpad: CanvasWorkpad; +} + +export const SimpleTemplate: FunctionComponent = ({ getArgValue, setArgValue, workpad }) => ( +
    + setArgValue('backgroundColor', color)} + colors={workpad.colors} + anchorPosition="leftCenter" + /> +
    +); + +SimpleTemplate.displayName = 'ContainerStyleArgSimpleInput'; + +SimpleTemplate.propTypes = { + getArgValue: PropTypes.func.isRequired, + setArgValue: PropTypes.func.isRequired, + workpad: PropTypes.shape({ + colors: PropTypes.array.isRequired, + }), +}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot new file mode 100644 index 000000000000..1486fe7cde14 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot @@ -0,0 +1,347 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots arguments/SeriesStyle extended 1`] = ` +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`Storyshots arguments/SeriesStyle/components extended: defaults 1`] = ` +
    +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot new file mode 100644 index 000000000000..ed6161b8ff6d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot @@ -0,0 +1,249 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots arguments/SeriesStyle simple 1`] = ` +
    +
    +
    + + Color  + +
    +
    + +
    +
    +
    +`; + +exports[`Storyshots arguments/SeriesStyle/components simple: defaults 1`] = ` +
    +
    +
    + + Color  + +
    +
    + +
    +
    +
    +`; + +exports[`Storyshots arguments/SeriesStyle/components simple: no labels 1`] = ` +
    +
    +
    + + Color  + +
    +
    + +
    +
    +
    +`; + +exports[`Storyshots arguments/SeriesStyle/components simple: no series 1`] = ` +
    +
    +
    + + Color  + +
    +
    + +
    +
    + + + +
    +
    +
    +`; + +exports[`Storyshots arguments/SeriesStyle/components simple: with series 1`] = ` +
    +
    +
    + + Color  + +
    +
    + +
    +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx new file mode 100644 index 000000000000..58af29463c3e --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx @@ -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 { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import { withKnobs, array, radios, boolean } from '@storybook/addon-knobs'; +import React from 'react'; + +import { ExtendedTemplate } from '../extended_template'; +import { ExpressionAST } from '../../../../../types'; + +const defaultExpression: ExpressionAST = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'seriesStyle', + arguments: {}, + }, + ], +}; + +const defaultValues = { + argValue: defaultExpression, +}; + +class Interactive extends React.Component<{}, { argValue: ExpressionAST }> { + public state = defaultValues; + + public render() { + const include = []; + if (boolean('Lines', true)) { + include.push('lines'); + } + if (boolean('Bars', true)) { + include.push('bars'); + } + if (boolean('Points', true)) { + include.push('points'); + } + return ( + { + action('onValueChange')(argValue); + this.setState({ argValue }); + }} + labels={array('Series Labels', ['label1', 'label2'])} + typeInstance={{ + name: radios('Type Instance', { default: 'defaultStyle', custom: 'custom' }, 'custom'), + options: { + include, + }, + }} + /> + ); + } +} + +storiesOf('arguments/SeriesStyle', module) + .addDecorator(story => ( +
    {story()}
    + )) + .addDecorator(withKnobs) + .add('extended', () => ); + +storiesOf('arguments/SeriesStyle/components', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('extended: defaults', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx new file mode 100644 index 000000000000..7a35f4de7980 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +// @ts-ignore Untyped local +import { getDefaultWorkpad } from '../../../../state/defaults'; + +import { SimpleTemplate } from '../simple_template'; +import { ExpressionAST } from '../../../../../types'; + +const defaultExpression: ExpressionAST = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'seriesStyle', + arguments: {}, + }, + ], +}; + +const defaultValues = { + argValue: defaultExpression, +}; + +class Interactive extends React.Component<{}, { argValue: ExpressionAST }> { + public state = defaultValues; + + public render() { + return ( + { + action('onValueChange')(argValue); + this.setState({ argValue }); + }} + workpad={getDefaultWorkpad()} + typeInstance={{ + name: 'defaultStyle', + }} + /> + ); + } +} + +storiesOf('arguments/SeriesStyle', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('simple', () => ); + +storiesOf('arguments/SeriesStyle/components', module) + .addDecorator(story => ( +
    {story()}
    + )) + .add('simple: no labels', () => ( + + )) + .add('simple: defaults', () => ( + + )) + .add('simple: no series', () => ( + + )) + .add('simple: with series', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.js deleted file mode 100644 index 8a28f00cfba8..000000000000 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.js +++ /dev/null @@ -1,103 +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 PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; -import { set, del } from 'object-path-immutable'; -import { get } from 'lodash'; - -export const ExtendedTemplate = props => { - const { typeInstance, onValueChange, labels, argValue } = props; - const chain = get(argValue, 'chain.0', {}); - const chainArgs = get(chain, 'arguments', {}); - const selectedSeries = get(chainArgs, 'label.0', ''); - const { name } = typeInstance; - const fields = get(typeInstance, 'options.include', []); - const hasPropFields = fields.some(field => ['lines', 'bars', 'points'].indexOf(field) !== -1); - - const handleChange = (argName, ev) => { - const fn = ev.target.value === '' ? del : set; - - const newValue = fn(argValue, ['chain', 0, 'arguments', argName], [ev.target.value]); - return onValueChange(newValue); - }; - - // TODO: add fill and stack options - // TODO: add label name auto-complete - const values = [ - { value: 0, text: 'None' }, - { value: 1, text: '1' }, - { value: 2, text: '2' }, - { value: 3, text: '3' }, - { value: 4, text: '4' }, - { value: 5, text: '5' }, - ]; - - const labelOptions = [{ value: '', text: 'Select Series' }]; - labels.sort().forEach(val => labelOptions.push({ value: val, text: val })); - - return ( -
    - {name !== 'defaultStyle' && ( - - handleChange('label', ev)} - /> - - )} - {hasPropFields && ( - - {fields.includes('lines') && ( - - - handleChange('lines', ev)} - /> - - - )} - {fields.includes('bars') && ( - - - handleChange('bars', ev)} - /> - - - )} - {fields.includes('points') && ( - - - handleChange('points', ev)} - /> - - - )} - - )} -
    - ); -}; - -ExtendedTemplate.displayName = 'SeriesStyleArgAdvancedInput'; - -ExtendedTemplate.propTypes = { - onValueChange: PropTypes.func.isRequired, - argValue: PropTypes.any.isRequired, - typeInstance: PropTypes.object, - labels: PropTypes.array.isRequired, - renderError: PropTypes.func, -}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx new file mode 100644 index 000000000000..7b625dadbbf6 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/extended_template.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, ChangeEvent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +import immutable from 'object-path-immutable'; +import { get } from 'lodash'; +import { ExpressionAST } from '../../../../types'; + +const { set, del } = immutable; + +export interface Arguments { + label: string; + lines: number; + bars: number; + points: number; +} +export type Argument = keyof Arguments; + +export interface Props { + argValue: ExpressionAST; + labels: string[]; + onValueChange: (argValue: ExpressionAST) => void; + typeInstance?: { + name: string; + options: { + include: string[]; + }; + }; +} + +export const ExtendedTemplate: FunctionComponent = props => { + const { typeInstance, onValueChange, labels, argValue } = props; + const chain = get(argValue, 'chain.0', {}); + const chainArgs = get(chain, 'arguments', {}); + const selectedSeries = get(chainArgs, 'label.0', ''); + + let name = ''; + if (typeInstance) { + name = typeInstance.name; + } + + const fields = get(typeInstance, 'options.include', []); + const hasPropFields = fields.some(field => ['lines', 'bars', 'points'].indexOf(field) !== -1); + + const handleChange: (key: T, val: ChangeEvent) => void = ( + argName, + ev + ) => { + const fn = ev.target.value === '' ? del : set; + const newValue = fn(argValue, `chain.0.arguments.${argName}`, [ev.target.value]); + return onValueChange(newValue); + }; + + // TODO: add fill and stack options + // TODO: add label name auto-complete + const values = [ + { value: 0, text: 'None' }, + { value: 1, text: '1' }, + { value: 2, text: '2' }, + { value: 3, text: '3' }, + { value: 4, text: '4' }, + { value: 5, text: '5' }, + ]; + + const labelOptions = [{ value: '', text: 'Select Series' }]; + labels.sort().forEach(val => labelOptions.push({ value: val, text: val })); + + return ( +
    + {name !== 'defaultStyle' && ( + + handleChange('label', ev)} + /> + + )} + {hasPropFields && ( + + {fields.includes('lines') && ( + + + handleChange('lines', ev)} + /> + + + )} + {fields.includes('bars') && ( + + + handleChange('bars', ev)} + /> + + + )} + {fields.includes('points') && ( + + + handleChange('points', ev)} + /> + + + )} + + )} +
    + ); +}; + +ExtendedTemplate.displayName = 'SeriesStyleArgAdvancedInput'; + +ExtendedTemplate.propTypes = { + onValueChange: PropTypes.func.isRequired, + argValue: PropTypes.any.isRequired, + typeInstance: PropTypes.object, + labels: PropTypes.array.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.js deleted file mode 100644 index da751fb5bc59..000000000000 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.js +++ /dev/null @@ -1,48 +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 PropTypes from 'prop-types'; -import { lifecycle } from 'recompose'; -import { get } from 'lodash'; -import { templateFromReactComponent } from '../../../lib/template_from_react_component'; -import { SimpleTemplate } from './simple_template'; -import { ExtendedTemplate } from './extended_template'; - -const EnhancedExtendedTemplate = lifecycle({ - formatLabel(label) { - if (typeof label !== 'string') { - this.props.renderError(); - } - return `Style: ${label}`; - }, - componentWillMount() { - const label = get(this.props.argValue, 'chain.0.arguments.label.0', ''); - if (label) { - this.props.setLabel(this.formatLabel(label)); - } - }, - componentWillReceiveProps(newProps) { - const newLabel = get(newProps.argValue, 'chain.0.arguments.label.0', ''); - if (newLabel && this.props.label !== this.formatLabel(newLabel)) { - this.props.setLabel(this.formatLabel(newLabel)); - } - }, -})(ExtendedTemplate); - -EnhancedExtendedTemplate.propTypes = { - argValue: PropTypes.any.isRequired, - setLabel: PropTypes.func.isRequired, - label: PropTypes.string, -}; - -export const seriesStyle = () => ({ - name: 'seriesStyle', - displayName: 'Series style', - help: 'Set the style for a selected named series', - template: templateFromReactComponent(EnhancedExtendedTemplate), - simpleTemplate: templateFromReactComponent(SimpleTemplate), - default: '{seriesStyle}', -}); diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.ts b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.ts new file mode 100644 index 000000000000..d13729568fde --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/index.ts @@ -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 PropTypes from 'prop-types'; +import { lifecycle, compose } from 'recompose'; +import { get } from 'lodash'; +import { templateFromReactComponent } from '../../../lib/template_from_react_component'; +import { SimpleTemplate } from './simple_template'; +import { ExtendedTemplate, Props as ExtendedTemplateProps } from './extended_template'; +import { ExpressionAST } from '../../../../types'; + +interface Props { + argValue: ExpressionAST; + renderError: Function; + setLabel: Function; + label: string; +} + +const formatLabel = (label: string, props: Props) => { + if (typeof label !== 'string') { + props.renderError(); + } + return `Style: ${label}`; +}; + +const EnhancedExtendedTemplate = compose( + lifecycle({ + componentWillMount() { + const label = get(this.props.argValue, 'chain.0.arguments.label.0', ''); + if (label) { + this.props.setLabel(formatLabel(label, this.props)); + } + }, + componentWillReceiveProps(newProps) { + const newLabel = get(newProps.argValue, 'chain.0.arguments.label.0', ''); + if (newLabel && this.props.label !== formatLabel(newLabel, this.props)) { + this.props.setLabel(formatLabel(newLabel, this.props)); + } + }, + }) +)(ExtendedTemplate); + +EnhancedExtendedTemplate.propTypes = { + argValue: PropTypes.any.isRequired, + setLabel: PropTypes.func.isRequired, + label: PropTypes.string, +}; + +export const seriesStyle = () => ({ + name: 'seriesStyle', + displayName: 'Series style', + help: 'Set the style for a selected named series', + template: templateFromReactComponent(EnhancedExtendedTemplate), + simpleTemplate: templateFromReactComponent(SimpleTemplate), + default: '{seriesStyle}', +}); diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.js deleted file mode 100644 index be3518494114..000000000000 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.js +++ /dev/null @@ -1,91 +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, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiButtonIcon } from '@elastic/eui'; -import { set, del } from 'object-path-immutable'; -import { get } from 'lodash'; -import { ColorPickerPopover } from '../../../components/color_picker_popover'; -import { TooltipIcon } from '../../../components/tooltip_icon'; - -export const SimpleTemplate = props => { - const { typeInstance, argValue, onValueChange, labels, workpad } = props; - const { name } = typeInstance; - const chain = get(argValue, 'chain.0', {}); - const chainArgs = get(chain, 'arguments', {}); - const color = get(chainArgs, 'color.0', ''); - - const handleChange = (argName, ev) => { - const fn = ev.target.value === '' ? del : set; - - const newValue = fn(argValue, ['chain', 0, 'arguments', argName], [ev.target.value]); - return onValueChange(newValue); - }; - - const handlePlain = (argName, val) => handleChange(argName, { target: { value: val } }); - - return ( - - {!color || color.length === 0 ? ( - - - Color  - - - handlePlain('color', '#000000')}> - Auto - - - - ) : ( - - - - - - handlePlain('color', val)} - colors={workpad.colors} - placement="leftCenter" - /> - - - handlePlain('color', '')} - aria-label="Remove Series Color" - /> - - - )} - {name !== 'defaultStyle' && (!labels || labels.length === 0) && ( - - - - )} - - ); -}; - -SimpleTemplate.displayName = 'SeriesStyleArgSimpleInput'; - -SimpleTemplate.propTypes = { - onValueChange: PropTypes.func.isRequired, - argValue: PropTypes.any.isRequired, - labels: PropTypes.array, - workpad: PropTypes.shape({ - colors: PropTypes.array.isRequired, - }).isRequired, - typeInstance: PropTypes.shape({ name: PropTypes.string.isRequired }).isRequired, -}; diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx new file mode 100644 index 000000000000..8bcf9d73daa5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/simple_template.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiButtonIcon } from '@elastic/eui'; +import immutable from 'object-path-immutable'; +import { get } from 'lodash'; +import { ColorPickerPopover } from '../../../components/color_picker_popover'; +// @ts-ignore Untyped local +import { TooltipIcon } from '../../../components/tooltip_icon'; +import { ExpressionAST, CanvasWorkpad } from '../../../../types'; + +const { set, del } = immutable; + +interface Arguments { + color: string; +} +type Argument = keyof Arguments; + +interface Props { + argValue: ExpressionAST; + labels?: string[]; + onValueChange: (argValue: ExpressionAST) => void; + typeInstance: { + name: string; + }; + workpad: CanvasWorkpad; +} + +export const SimpleTemplate: FunctionComponent = props => { + const { typeInstance, argValue, onValueChange, labels, workpad } = props; + const { name } = typeInstance; + const chain = get(argValue, 'chain.0', {}); + const chainArgs = get(chain, 'arguments', {}); + const color: string = get(chainArgs, 'color.0', ''); + + const handleChange: (key: T, val: string) => void = (argName, val) => { + const fn = val === '' ? del : set; + const newValue = fn(argValue, `chain.0.arguments.${argName}`, [val]); + return onValueChange(newValue); + }; + + return ( + + {!color || color.length === 0 ? ( + + + Color  + + + handleChange('color', '#000000')}> + Auto + + + + ) : ( + + + + + + handleChange('color', val)} + value={color} + /> + + + handleChange('color', '')} + aria-label="Remove Series Color" + /> + + + )} + {name !== 'defaultStyle' && (!labels || labels.length === 0) && ( + + + + )} + + ); +}; + +SimpleTemplate.displayName = 'SeriesStyleArgSimpleInput'; + +SimpleTemplate.propTypes = { + argValue: PropTypes.any.isRequired, + labels: PropTypes.array, + onValueChange: PropTypes.func.isRequired, + workpad: PropTypes.shape({ + colors: PropTypes.array.isRequired, + }).isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts b/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts new file mode 100644 index 000000000000..33f3d801c22d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.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. + */ + +import chrome from 'ui/chrome'; + +export const AdvancedSettings = chrome.getUiSettingsClient(); diff --git a/x-pack/legacy/plugins/canvas/public/lib/sync_filter_expression.ts b/x-pack/legacy/plugins/canvas/public/lib/sync_filter_expression.ts index 8252fe948bd6..dc70f778f0e5 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/sync_filter_expression.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/sync_filter_expression.ts @@ -6,10 +6,11 @@ // @ts-ignore internal untyped import { fromExpression } from '@kbn/interpreter/common'; -// @ts-ignore external untyped -import { set, del } from 'object-path-immutable'; +import immutable from 'object-path-immutable'; import { get } from 'lodash'; +const { set, del } = immutable; + export function syncFilterExpression( config: Record, filterExpression: string, diff --git a/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.js b/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.js deleted file mode 100644 index d4462e94320b..000000000000 --- a/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.js +++ /dev/null @@ -1,45 +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 ReactDom from 'react-dom'; -import PropTypes from 'prop-types'; -import { ErrorBoundary } from '../components/enhance/error_boundary'; - -export const templateFromReactComponent = Component => { - const WrappedComponent = props => ( - - {({ error }) => { - if (error) { - props.renderError(); - return null; - } - - return ; - }} - - ); - - WrappedComponent.propTypes = { - renderError: PropTypes.func, - }; - - return (domNode, config, handlers) => { - try { - const el = React.createElement(WrappedComponent, config); - ReactDom.render(el, domNode, () => { - handlers.done(); - }); - - handlers.onDestroy(() => { - ReactDom.unmountComponentAtNode(domNode); - }); - } catch (err) { - handlers.done(); - config.renderError(); - } - }; -}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.tsx b/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.tsx new file mode 100644 index 000000000000..5144da587fa7 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.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, { ComponentType, FunctionComponent } from 'react'; +import { unmountComponentAtNode, render } from 'react-dom'; +import PropTypes from 'prop-types'; +import { ErrorBoundary } from '../components/enhance/error_boundary'; + +interface Props { + renderError: Function; +} + +interface Handlers { + done: () => void; + onDestroy: (fn: () => void) => void; +} + +export const templateFromReactComponent = (Component: ComponentType) => { + const WrappedComponent: FunctionComponent = props => ( + + {({ error }: { error: Error }) => { + if (error) { + props.renderError(); + return null; + } + + return ; + }} + + ); + + WrappedComponent.propTypes = { + renderError: PropTypes.func, + }; + + return (domNode: Element, config: Props, handlers: Handlers) => { + try { + const el = React.createElement(WrappedComponent, config); + render(el, domNode, () => { + handlers.done(); + }); + + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + } catch (err) { + handlers.done(); + config.renderError(); + } + }; +}; diff --git a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js index cad58aa781f4..f683ae9caa08 100644 --- a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js @@ -6,7 +6,7 @@ import { createAction } from 'redux-actions'; import { createThunk } from 'redux-thunks'; -import { set, del } from 'object-path-immutable'; +import immutable from 'object-path-immutable'; import { get, pick, cloneDeep, without } from 'lodash'; import { toExpression, safeElementFromExpression } from '@kbn/interpreter/common'; import { interpretAst } from 'plugins/interpreter/interpreter'; @@ -19,6 +19,8 @@ import { subMultitree } from '../../lib/aeroelastic/functional'; import { selectToplevelNodes } from './transient'; import * as args from './resolved_args'; +const { set, del } = immutable; + export function getSiblingContext(state, elementId, checkIndex) { const prevContextPath = [elementId, 'expressionContext', checkIndex]; const prevContextValue = getResolvedArgsValue(state, prevContextPath); @@ -61,7 +63,7 @@ export const flushContextAfterIndex = createAction('flushContextAfterIndex'); export const fetchContext = createThunk( 'fetchContext', ({ dispatch, getState }, index, element, fullRefresh = false) => { - const chain = get(element, ['ast', 'chain']); + const chain = get(element, 'ast.chain'); const invalidIndex = chain ? index >= chain.length : true; if (!element || !chain || invalidIndex) { @@ -301,7 +303,7 @@ export const setAstAtIndex = createThunk( // invalidate cached context for elements after this index dispatch(flushContextAfterIndex({ elementId: element.id, index })); - const newElement = set(element, ['ast', 'chain', index], ast); + const newElement = set(element, `ast.chain.${index}`, ast); const newAst = get(newElement, 'ast'); // fetch renderable using existing context, if available (value is null if not cached) @@ -340,9 +342,9 @@ export const setAstAtIndex = createThunk( // the entire argument from be set to the passed value export const setArgumentAtIndex = createThunk('setArgumentAtIndex', ({ dispatch }, args) => { const { index, argName, value, valueIndex, element, pageId } = args; - const selector = ['ast', 'chain', index, 'arguments', argName]; + let selector = `ast.chain.${index}.arguments.${argName}`; if (valueIndex != null) { - selector.push(valueIndex); + selector += '.' + valueIndex; } const newElement = set(element, selector, value); @@ -380,9 +382,9 @@ export const deleteArgumentAtIndex = createThunk('deleteArgumentAtIndex', ({ dis const newElement = argIndex != null && curVal.length > 1 ? // if more than one val, remove the specified val - del(element, ['ast', 'chain', index, 'arguments', argName, argIndex]) + del(element, `ast.chain.${index}.arguments.${argName}.${argIndex}`) : // otherwise, remove the entire key - del(element, ['ast', 'chain', index, 'arguments', argName]); + del(element, `ast.chain.${index}.arguments.${argName}`); dispatch(setAstAtIndex(index, get(newElement, ['ast', 'chain', index]), element, pageId)); }); diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/assets.js b/x-pack/legacy/plugins/canvas/public/state/reducers/assets.js index 9aa3cd225ac2..ee8f81d8bd9f 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/assets.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/assets.js @@ -5,11 +5,13 @@ */ import { handleActions, combineActions } from 'redux-actions'; -import { set, assign, del } from 'object-path-immutable'; +import immutable from 'object-path-immutable'; import { get } from 'lodash'; import { createAsset, setAssetValue, removeAsset, setAssets, resetAssets } from '../actions/assets'; import { getId } from '../../lib/get_id'; +const { set, assign, del } = immutable; + export const assetsReducer = handleActions( { [createAsset]: (assetState, { payload }) => { @@ -34,7 +36,7 @@ export const assetsReducer = handleActions( return del(assetState, assetId); }, - [combineActions(setAssets, resetAssets)]: (assetState, { payload }) => payload || {}, + [combineActions(setAssets, resetAssets)]: (_assetState, { payload }) => payload || {}, }, {} ); diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/elements.js b/x-pack/legacy/plugins/canvas/public/state/reducers/elements.js index 4175d2bcdf33..10a5bdb5998e 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/elements.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/elements.js @@ -5,10 +5,12 @@ */ import { handleActions } from 'redux-actions'; -import { assign, push, del, set } from 'object-path-immutable'; +import immutable from 'object-path-immutable'; import { get } from 'lodash'; import * as actions from '../actions/elements'; +const { assign, push, del, set } = immutable; + const getLocation = type => (type === 'group' ? 'groups' : 'elements'); const firstOccurrence = (element, index, array) => array.indexOf(element) === index; @@ -29,14 +31,14 @@ function getNodeIndexById(page, nodeId, location) { function assignNodeProperties(workpadState, pageId, nodeId, props) { const pageIndex = getPageIndexById(workpadState, pageId); const location = getLocationFromIds(workpadState, pageId, nodeId); - const nodesPath = ['pages', pageIndex, location]; + const nodesPath = `pages.${pageIndex}.${location}`; const nodeIndex = get(workpadState, nodesPath, []).findIndex(node => node.id === nodeId); if (pageIndex === -1 || nodeIndex === -1) { return workpadState; } - return assign(workpadState, nodesPath.concat(nodeIndex), props); + return assign(workpadState, `${nodesPath}.${nodeIndex}`, props); } function moveNodeLayer(workpadState, pageId, nodeId, movement, location) { @@ -66,7 +68,7 @@ function moveNodeLayer(workpadState, pageId, nodeId, movement, location) { const newNodes = nodes.slice(0); newNodes.splice(to, 0, newNodes.splice(from, 1)[0]); - return set(workpadState, ['pages', pageIndex, location], newNodes); + return set(workpadState, `pages.${pageIndex}.${location}`, newNodes); } const trimPosition = ({ left, top, width, height, angle, parent }) => ({ @@ -123,7 +125,7 @@ export const elementsReducer = handleActions( } return push( workpadState, - ['pages', pageIndex, getLocation(element.position.type)], + `pages.${pageIndex}.${getLocation(element.position.type)}`, trimElement(element) ); }, @@ -136,7 +138,7 @@ export const elementsReducer = handleActions( (state, element) => push( state, - ['pages', pageIndex, getLocation(element.position.type)], + `pages.${pageIndex}.${getLocation(element.position.type)}`, trimElement(element) ), workpadState @@ -160,7 +162,7 @@ export const elementsReducer = handleActions( .sort((a, b) => b.index - a.index); // deleting from end toward beginning, otherwise indices will become off - todo fuse loops! return nodeIndices.reduce((state, { location, index }) => { - return del(state, ['pages', pageIndex, location, index]); + return del(state, `pages.${pageIndex}.${location}.${index}`); }, workpadState); }, }, diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/pages.js b/x-pack/legacy/plugins/canvas/public/state/reducers/pages.js index c2bcb5b93948..224d0a3c0379 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/pages.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/pages.js @@ -5,7 +5,7 @@ */ import { handleActions } from 'redux-actions'; -import { set, del, insert } from 'object-path-immutable'; +import immutable from 'object-path-immutable'; import { cloneSubgraphs } from '../../lib/clone_subgraphs'; import { getId } from '../../lib/get_id'; import { routerProvider } from '../../lib/router_provider'; @@ -14,6 +14,8 @@ import * as actions from '../actions/pages'; import { getSelectedPageIndex } from '../selectors/workpad'; import { isGroupId } from '../../components/workpad_page/integration_utils'; +const { set, del, insert } = immutable; + const setPageIndex = (workpadState, index) => index < 0 || !workpadState.pages[index] || getSelectedPageIndex(workpadState) === index ? workpadState @@ -143,12 +145,12 @@ export const pagesReducer = handleActions( [actions.stylePage]: (workpadState, { payload }) => { const pageIndex = workpadState.pages.findIndex(page => page.id === payload.pageId); - return set(workpadState, ['pages', pageIndex, 'style'], payload.style); + return set(workpadState, `pages.${pageIndex}.style`, payload.style); }, [actions.setPageTransition]: (workpadState, { payload }) => { const pageIndex = workpadState.pages.findIndex(page => page.id === payload.pageId); - return set(workpadState, ['pages', pageIndex, 'transition'], payload.transition); + return set(workpadState, `pages.${pageIndex}.transition`, payload.transition); }, }, {} diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/resolved_args.js b/x-pack/legacy/plugins/canvas/public/state/reducers/resolved_args.js index f458a3b57233..fa0be7702765 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/resolved_args.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/resolved_args.js @@ -5,13 +5,14 @@ */ import { handleActions } from 'redux-actions'; -import { set, del } from 'object-path-immutable'; +import immutable from 'object-path-immutable'; import { get } from 'lodash'; import { prepend } from '../../lib/modify_path'; import * as actions from '../actions/resolved_args'; import { flushContext, flushContextAfterIndex } from '../actions/elements'; import { setWorkpad } from '../actions/workpad'; +const { set, del } = immutable; /* Resolved args are a way to handle async values. They track the status, value, and error state thgouh the lifecycle of the request, and are an object that looks like this: diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/transient.js b/x-pack/legacy/plugins/canvas/public/state/reducers/transient.js index e059e6439d86..0b89dfd3c956 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/transient.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/transient.js @@ -5,13 +5,15 @@ */ import { handleActions } from 'redux-actions'; -import { set, del } from 'object-path-immutable'; +import immutable from 'object-path-immutable'; import { restoreHistory } from '../actions/history'; import * as pageActions from '../actions/pages'; import * as transientActions from '../actions/transient'; import { removeElements } from '../actions/elements'; import { setRefreshInterval, enableAutoplay, setAutoplayInterval } from '../actions/workpad'; +const { set, del } = immutable; + export const transientReducer = handleActions( { // clear all the resolved args when restoring the history diff --git a/x-pack/legacy/plugins/canvas/types/canvas.ts b/x-pack/legacy/plugins/canvas/types/canvas.ts new file mode 100644 index 000000000000..97f8917d5072 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/types/canvas.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 { ElementPosition } from './elements'; + +export interface CanvasElement { + id: string; + position: ElementPosition; + type: 'element'; + expression: string; + filter: string; +} + +export interface CanvasPage { + id: string; + style: { + background: string; + }; + transition: {}; // Fix + elements: CanvasElement[]; + groups: CanvasElement[][]; +} + +export interface CanvasWorkpad { + name: string; + id: string; + width: number; + height: number; + css: string; + page: number; + pages: CanvasPage[]; + colors: string[]; + isWriteable: boolean; +} diff --git a/x-pack/legacy/plugins/canvas/types/elements.ts b/x-pack/legacy/plugins/canvas/types/elements.ts index ee98b4b91e40..35b3a28e1ae0 100644 --- a/x-pack/legacy/plugins/canvas/types/elements.ts +++ b/x-pack/legacy/plugins/canvas/types/elements.ts @@ -59,7 +59,7 @@ export interface AST { }>; } -interface Position { +export interface ElementPosition { /** * distance from the left edge of the page */ @@ -94,7 +94,7 @@ export interface PositionedElement { /** * layout engine settings */ - position: Position; + position: ElementPosition; /** * Canvas expression used to generate the element */ diff --git a/x-pack/legacy/plugins/canvas/types/functions.ts b/x-pack/legacy/plugins/canvas/types/functions.ts index ee0c17e10a19..cd1947443b23 100644 --- a/x-pack/legacy/plugins/canvas/types/functions.ts +++ b/x-pack/legacy/plugins/canvas/types/functions.ts @@ -116,28 +116,6 @@ export type CanvasFunction = FunctionFactory; */ export type CanvasFunctionName = CanvasFunction['name']; -/** - * Represents an object that is intended to be rendered. - */ -export interface Render { - type: 'render'; - as: string; - value: T; -} - -/** - * Represents an object that is a Filter. - */ -export interface Filter { - type?: string; - value?: string; - column?: string; - and: Filter[]; - to?: string; - from?: string; - query?: string | null; -} - /** * Represents a function called by the `case` Function. */ @@ -147,43 +125,6 @@ export interface Case { result: any; } -// DATATABLES -// ---------- - -/** - * A Utility function that Typescript can use to determine if an object is a Datatable. - * @param datatable - */ -export const isDatatable = (datatable: any): datatable is Datatable => - !!datatable && datatable.type === 'datatable'; - -/** - * This type represents the `type` of any `DatatableColumn` in a `Datatable`. - */ -export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; - -/** - * This type represents a `DatatableRow` in a `Datatable`. - */ -export type DatatableRow = Record; - -/** - * This type represents the shape of a column in a `Datatable`. - */ -export interface DatatableColumn { - name: string; - type: DatatableColumnType; -} - -/** - * A `Datatable` in Canvas is a unique structure that represents tabulated data. - */ -export interface Datatable { - type: 'datatable'; - columns: DatatableColumn[]; - rows: DatatableRow[]; -} - export enum Legend { NORTH_WEST = 'nw', SOUTH_WEST = 'sw', @@ -198,34 +139,6 @@ export enum Position { RIGHT = 'right', } -/** - * Allowed column names in a PointSeries - */ -export type PointSeriesColumnName = 'x' | 'y' | 'color' | 'size' | 'text'; - -/** - * Column in a PointSeries - */ -export interface PointSeriesColumn { - type: 'number' | 'string'; - role: 'measure' | 'dimension'; - expression: string; -} - -/** - * Represents a collection of valid Columns in a PointSeries - */ -export type PointSeriesColumns = { [key in PointSeriesColumnName]: PointSeriesColumn }; - -/** - * A `PointSeries` in Canvas is a unique structure that represents dots on a chart. - */ -export interface PointSeries { - type: 'pointseries'; - columns: PointSeriesColumns; - rows: Array>; -} - export interface SeriesStyle { type: 'seriesStyle'; bars: number; diff --git a/x-pack/legacy/plugins/canvas/types/index.ts b/x-pack/legacy/plugins/canvas/types/index.ts index d253322055af..99d3ea9b85b4 100644 --- a/x-pack/legacy/plugins/canvas/types/index.ts +++ b/x-pack/legacy/plugins/canvas/types/index.ts @@ -4,11 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from '../../../../../src/legacy/core_plugins/interpreter/public/types/style'; +export { + ContainerStyle, + Overflow, + BackgroundRepeat, + BackgroundSize, +} from '../../../../../src/legacy/core_plugins/interpreter/public/types/style'; +export * from '../../../../../src/plugins/data/common/expressions/types'; export * from './assets'; +export * from './canvas'; export * from './elements'; export * from './functions'; export * from './renderers'; export * from './shortcuts'; export * from './state'; +export * from './style'; export * from './telemetry'; diff --git a/x-pack/legacy/plugins/canvas/types/renderers.ts b/x-pack/legacy/plugins/canvas/types/renderers.ts index 2167c542c354..282a1c820e34 100644 --- a/x-pack/legacy/plugins/canvas/types/renderers.ts +++ b/x-pack/legacy/plugins/canvas/types/renderers.ts @@ -7,19 +7,32 @@ type GenericCallback = (callback: () => void) => void; export interface RendererHandlers { + /** Handler to invoke when an element has finished rendering */ done: () => void; - getFilter: () => string; + /** Handler to invoke when an element is deleted or changes to a different render type */ onDestroy: GenericCallback; + /** Handler to invoke when an element's dimensions have changed*/ onResize: GenericCallback; + /** Retrieves the value of the filter property on the element object persisted on the workpad */ + getFilter: () => string; + /** Sets the value of the filter property on the element object persisted on the workpad */ setFilter: (filter: string) => void; } export interface RendererSpec { + /** The render type */ name: string; + /** The name to display */ displayName: string; + /** A description of what is rendered */ help: string; + /** Indicate whether the element should reuse the existing DOM element when re-rendering */ reuseDomNode: boolean; - height: number; + /** The default width of the element in pixels */ + width?: number; + /** The default height of the element in pixels */ + height?: number; + /** A function that renders an element into the specified DOM element */ render: (domNode: HTMLElement, config: RendererConfig, handlers: RendererHandlers) => void; } diff --git a/x-pack/legacy/plugins/canvas/types/style.ts b/x-pack/legacy/plugins/canvas/types/style.ts new file mode 100644 index 000000000000..8484c506e28a --- /dev/null +++ b/x-pack/legacy/plugins/canvas/types/style.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum BorderStyle { + NONE = 'none', + SOLID = 'solid', + DOTTED = 'dotted', + DASHED = 'dashed', + DOUBLE = 'double', + GROOVE = 'groove', + RIDGE = 'ridge', + INSET = 'inset', + OUTSET = 'outset', +} + +export const isBorderStyle = (style: any): style is BorderStyle => + !!style && Object.values(BorderStyle).includes(style); diff --git a/x-pack/legacy/plugins/code/common/git_url_utils.ts b/x-pack/legacy/plugins/code/common/git_url_utils.ts index 27159cf91cc4..7ae90805dbdc 100644 --- a/x-pack/legacy/plugins/code/common/git_url_utils.ts +++ b/x-pack/legacy/plugins/code/common/git_url_utils.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import GitUrlParse from 'git-url-parse'; // return true if the git url is valid, otherwise throw Error with @@ -18,14 +19,22 @@ export function validateGitUrl( if (hostWhitelist && hostWhitelist.length > 0) { const hostSet = new Set(hostWhitelist); if (!hostSet.has(repo.source)) { - throw new Error('Git url host is not whitelisted.'); + throw new Error( + i18n.translate('xpack.code.gitUrlUtil.urlNotWhitelistedMessage', { + defaultMessage: 'Git url host is not whitelisted.', + }) + ); } } if (protocolWhitelist && protocolWhitelist.length > 0) { const protocolSet = new Set(protocolWhitelist); if (!protocolSet.has(repo.protocol)) { - throw new Error('Git url protocol is not whitelisted.'); + throw new Error( + i18n.translate('xpack.code.gitUrlUtil.protocolNotWhitelistedMessage', { + defaultMessage: 'Git url protocol is not whitelisted.', + }) + ); } } return true; diff --git a/x-pack/legacy/plugins/code/common/language_server.ts b/x-pack/legacy/plugins/code/common/language_server.ts index b07d413336ca..8e094d71d026 100644 --- a/x-pack/legacy/plugins/code/common/language_server.ts +++ b/x-pack/legacy/plugins/code/common/language_server.ts @@ -27,15 +27,29 @@ export interface LanguageServer { export const CTAGS_SUPPORT_LANGS = [ 'c', 'cpp', + 'clojure', 'csharp', + 'css', + 'go', + 'html', + 'ini', 'lua', + 'json', + 'objective-c', 'pascal', 'perl', 'php', 'python', + 'r', 'ruby', + 'rust', 'scheme', 'shell', 'sql', 'tcl', + 'typescript', + 'xml', + 'yaml', + 'java', + 'javascript', ]; diff --git a/x-pack/legacy/plugins/code/common/lsp_client.ts b/x-pack/legacy/plugins/code/common/lsp_client.ts index be99b0d641b5..f76d5ea80889 100644 --- a/x-pack/legacy/plugins/code/common/lsp_client.ts +++ b/x-pack/legacy/plugins/code/common/lsp_client.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { ResponseError, ResponseMessage } from './jsonrpc'; @@ -27,9 +27,7 @@ export class LspRestClient implements LspClient { signal?: AbortSignal ): Promise { try { - const response = await kfetch({ - pathname: `${this.baseUri}/${method}`, - method: 'POST', + const response = await npStart.core.http.post(`${this.baseUri}/${method}`, { body: JSON.stringify(params), signal, }); diff --git a/x-pack/legacy/plugins/code/common/repo_file_status.ts b/x-pack/legacy/plugins/code/common/repo_file_status.ts index e898e3ef2b93..d6901d6c53ba 100644 --- a/x-pack/legacy/plugins/code/common/repo_file_status.ts +++ b/x-pack/legacy/plugins/code/common/repo_file_status.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + export enum RepoFileStatus { LANG_SERVER_IS_INITIALIZING = 'Language server is initializing.', LANG_SERVER_INITIALIZED = 'Language server initialized.', @@ -27,6 +29,63 @@ export enum LangServerType { DEDICATED = 'Current file is covered by dedicated language server', } +export const RepoFileStatusText = { + [RepoFileStatus.LANG_SERVER_IS_INITIALIZING]: i18n.translate( + 'xpack.code.repoFileStatus.langugageServerIsInitializitingMessage', + { + defaultMessage: 'Language server is initializing.', + } + ), + [RepoFileStatus.LANG_SERVER_INITIALIZED]: i18n.translate( + 'xpack.code.repoFileStatus.languageServerInitializedMessage', + { + defaultMessage: 'Language server initialized.', + } + ), + [RepoFileStatus.INDEXING]: i18n.translate('xpack.code.repoFileStatus.IndexingInProgressMessage', { + defaultMessage: 'Indexing in progress.', + }), + [RepoFileStatus.FILE_NOT_SUPPORTED]: i18n.translate( + 'xpack.code.repoFileStatus.fileNotSupportedMessage', + { + defaultMessage: 'Current file is not of a supported language.', + } + ), + [RepoFileStatus.REVISION_NOT_INDEXED]: i18n.translate( + 'xpack.code.repoFileStatus.revisionNotIndexedMessage', + { + defaultMessage: 'Current revision is not indexed.', + } + ), + [RepoFileStatus.LANG_SERVER_NOT_INSTALLED]: i18n.translate( + 'xpack.code.repoFileStatus.langServerNotInstalledMessage', + { + defaultMessage: 'Install additional language server to support current file.', + } + ), + [RepoFileStatus.FILE_IS_TOO_BIG]: i18n.translate( + 'xpack.code.repoFileStatus.fileIsTooBigMessage', + { + defaultMessage: 'Current file is too big.', + } + ), + [LangServerType.NONE]: i18n.translate('xpack.code.repoFileStatus.langserverType.noneMessage', { + defaultMessage: 'Current file is not supported by any language server.', + }), + [LangServerType.GENERIC]: i18n.translate( + 'xpack.code.repoFileStatus.langserverType.genericMessage', + { + defaultMessage: 'Current file is only covered by generic language server.', + } + ), + [LangServerType.DEDICATED]: i18n.translate( + 'xpack.code.repoFileStatus.langserverType.dedicatedMessage', + { + defaultMessage: 'Current file is covered by dedicated language server.', + } + ), +}; + export enum CTA { SWITCH_TO_HEAD, GOTO_LANG_MANAGE_PAGE, diff --git a/x-pack/legacy/plugins/code/index.ts b/x-pack/legacy/plugins/code/index.ts index 79fc741d863b..5dc045aa59b1 100644 --- a/x-pack/legacy/plugins/code/index.ts +++ b/x-pack/legacy/plugins/code/index.ts @@ -4,14 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; +import { RequestQuery, ResponseToolkit, RouteOptions, ServerRoute } from 'hapi'; import JoiNamespace from 'joi'; +import { Legacy } from 'kibana'; import moment from 'moment'; import { resolve } from 'path'; -import { init } from './server/init'; +import { CoreSetup, PluginInitializerContext } from 'src/core/server'; import { APP_TITLE } from './common/constants'; import { LanguageServers, LanguageServersDeveloping } from './server/lsp/language_servers'; +import { codePlugin } from './server'; + +export type RequestFacade = Legacy.Request; +export type RequestQueryFacade = RequestQuery; +export type ResponseToolkitFacade = ResponseToolkit; +export type RouteOptionsFacade = RouteOptions; +export type ServerFacade = Legacy.Server; +export type ServerRouteFacade = ServerRoute; export const code = (kibana: any) => new kibana.Plugin({ @@ -23,11 +32,11 @@ export const code = (kibana: any) => uiExports: { app: { title: APP_TITLE, - main: 'plugins/code/app', + main: 'plugins/code/index', euiIconType: 'codeApp', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - injectDefaultVars(server: Server) { + injectDefaultVars(server: ServerFacade) { const config = server.config(); return { codeUiEnabled: config.get('xpack.code.ui.enabled'), @@ -93,11 +102,37 @@ export const code = (kibana: any) => .default(['https', 'git', 'ssh']), enableGitCertCheck: Joi.boolean().default(true), }).default(), + disk: Joi.object({ + thresholdEnabled: Joi.bool().default(true), + watermarkLowMb: Joi.number().default(2048), + }).default(), maxWorkspace: Joi.number().default(5), // max workspace folder for each language server - disableIndexScheduler: Joi.boolean().default(false), enableGlobalReference: Joi.boolean().default(false), // Global reference as optional feature for now codeNodeUrl: Joi.string(), }).default(); }, - init, + init(server: ServerFacade, options: any) { + if (!options.ui.enabled) { + return; + } + + const initializerContext = {} as PluginInitializerContext; + const coreSetup = ({ + http: { server }, + } as any) as CoreSetup; + + // Set up with the new platform plugin lifecycle API. + const plugin = codePlugin(initializerContext); + plugin.setup(coreSetup, options); + + // @ts-ignore + const kbnServer = this.kbnServer; + kbnServer.ready().then(async () => { + await plugin.start(coreSetup); + }); + + server.events.on('stop', async () => { + await plugin.stop(); + }); + }, }); diff --git a/x-pack/legacy/plugins/code/kibana.json b/x-pack/legacy/plugins/code/kibana.json new file mode 100644 index 000000000000..03fd3694df53 --- /dev/null +++ b/x-pack/legacy/plugins/code/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "code", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": [ + "elasticsearch", + "xpack_main" + ] +} diff --git a/x-pack/legacy/plugins/code/public/actions/file.ts b/x-pack/legacy/plugins/code/public/actions/file.ts index da6db3962095..c1f27cdc0a9f 100644 --- a/x-pack/legacy/plugins/code/public/actions/file.ts +++ b/x-pack/legacy/plugins/code/public/actions/file.ts @@ -58,10 +58,6 @@ export const fetchRepoTree = createAction('FETCH REPO TREE export const fetchRepoTreeSuccess = createAction('FETCH REPO TREE SUCCESS'); export const fetchRepoTreeFailed = createAction('FETCH REPO TREE FAILED'); -export const resetRepoTree = createAction('CLEAR REPO TREE'); -export const closeTreePath = createAction('CLOSE TREE PATH'); -export const openTreePath = createAction('OPEN TREE PATH'); - export const fetchRepoBranches = createAction('FETCH REPO BRANCHES'); export const fetchRepoBranchesSuccess = createAction( 'FETCH REPO BRANCHES SUCCESS' diff --git a/x-pack/legacy/plugins/code/public/actions/route.ts b/x-pack/legacy/plugins/code/public/actions/route.ts index f2d9708cc09e..3adcdc6111d1 100644 --- a/x-pack/legacy/plugins/code/public/actions/route.ts +++ b/x-pack/legacy/plugins/code/public/actions/route.ts @@ -7,6 +7,6 @@ import { createAction } from 'redux-actions'; export const routePathChange = createAction('ROUTE PATH CHANGE'); -export const repoChange = createAction('REPOSITORY CHANGE'); +export const repoChange = createAction('REPOSITORY CHANGE'); export const revisionChange = createAction('REVISION CHANGE'); export const filePathChange = createAction('FILE PATH CHANGE'); diff --git a/x-pack/legacy/plugins/code/public/actions/structure.ts b/x-pack/legacy/plugins/code/public/actions/structure.ts index c3114a8d9126..fcf6cb43b5d9 100644 --- a/x-pack/legacy/plugins/code/public/actions/structure.ts +++ b/x-pack/legacy/plugins/code/public/actions/structure.ts @@ -5,16 +5,16 @@ */ import { createAction } from 'redux-actions'; -import { SymbolInformation } from 'vscode-languageserver-types/lib/esm/main'; +import { DocumentSymbol } from 'vscode-languageserver-types'; -export interface SymbolWithMembers extends SymbolInformation { +export interface SymbolWithMembers extends DocumentSymbol { members?: SymbolWithMembers[]; path?: string; } export interface SymbolsPayload { path: string; - data: SymbolInformation[]; + data: DocumentSymbol[]; structureTree: SymbolWithMembers[]; } diff --git a/x-pack/legacy/plugins/code/public/app.tsx b/x-pack/legacy/plugins/code/public/app.tsx index 994d0120f9bd..d0e84bd843d9 100644 --- a/x-pack/legacy/plugins/code/public/app.tsx +++ b/x-pack/legacy/plugins/code/public/app.tsx @@ -7,57 +7,71 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Provider } from 'react-redux'; + +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; +import moment from 'moment'; +import { CoreStart } from 'src/core/public'; import 'ui/autoload/all'; import 'ui/autoload/styles'; import chrome from 'ui/chrome'; // @ts-ignore import { uiModules } from 'ui/modules'; + import { APP_TITLE } from '../common/constants'; import { App } from './components/app'; import { HelpMenu } from './components/help_menu'; import { store } from './stores'; -if (chrome.getInjected('codeUiEnabled')) { - const app = uiModules.get('apps/code'); +export function startApp(coreStart: CoreStart) { + // `getInjected` is not currently available in new platform `coreStart.chrome` + if (chrome.getInjected('codeUiEnabled')) { + // TODO the entire Kibana uses moment, we might need to move it to a more common place + moment.locale(i18n.getLocale()); - app.config(($locationProvider: any) => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, + const app = uiModules.get('apps/code'); + app.config(($locationProvider: any) => { + $locationProvider.html5Mode({ + enabled: false, + requireBase: false, + rewriteLinks: false, + }); }); - }); - app.config((stateManagementConfigProvider: any) => stateManagementConfigProvider.disable()); - - function RootController($scope: any, $element: any, $http: any) { - const domNode = $element[0]; - - // render react to DOM - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); + app.config((stateManagementConfigProvider: any) => stateManagementConfigProvider.disable()); + + function RootController($scope: any, $element: any, $http: any) { + const domNode = $element[0]; + + // render react to DOM + render( + + + + + , + domNode + ); + + // unmount react on controller destroy + $scope.$on('$destroy', () => { + unmountComponentAtNode(domNode); + }); + } + + // `setRootController` is not available now in `coreStart.chrome` + chrome.setRootController('code', RootController); + coreStart.chrome.setBreadcrumbs([ + { + text: APP_TITLE, + href: '#/', + }, + ]); + + coreStart.chrome.setHelpExtension(domNode => { + render(, domNode); + return () => { + unmountComponentAtNode(domNode); + }; }); } - - chrome.setRootController('code', RootController); - chrome.breadcrumbs.set([ - { - text: APP_TITLE, - href: '#/', - }, - ]); - - chrome.helpExtension.set(domNode => { - render(, domNode); - return () => { - unmountComponentAtNode(domNode); - }; - }); } diff --git a/x-pack/legacy/plugins/code/public/common/types.ts b/x-pack/legacy/plugins/code/public/common/types.ts index c2122022b9d8..e131c231562c 100644 --- a/x-pack/legacy/plugins/code/public/common/types.ts +++ b/x-pack/legacy/plugins/code/public/common/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { ReactNode } from 'react'; import { SearchScope } from '../../model'; @@ -15,17 +16,33 @@ export enum PathTypes { } export const SearchScopeText = { - [SearchScope.DEFAULT]: 'Search Everything', - [SearchScope.REPOSITORY]: 'Search Repositories', - [SearchScope.SYMBOL]: 'Search Symbols', - [SearchScope.FILE]: 'Search Files', + [SearchScope.DEFAULT]: i18n.translate('xpack.code.searchScope.defaultDropDownOptionLabel', { + defaultMessage: 'Search Everything', + }), + [SearchScope.REPOSITORY]: i18n.translate('xpack.code.searchScope.repositoryDropDownOptionLabel', { + defaultMessage: 'Search Repositories', + }), + [SearchScope.SYMBOL]: i18n.translate('xpack.code.searchScope.symbolDropDownOptionLabel', { + defaultMessage: 'Search Symbols', + }), + [SearchScope.FILE]: i18n.translate('xpack.code.searchScope.fileDropDownOptionLabel', { + defaultMessage: 'Search Files', + }), }; export const SearchScopePlaceholderText = { - [SearchScope.DEFAULT]: 'Type to find anything', - [SearchScope.REPOSITORY]: 'Type to find repositories', - [SearchScope.SYMBOL]: 'Type to find symbols', - [SearchScope.FILE]: 'Type to find files', + [SearchScope.DEFAULT]: i18n.translate('xpack.code.searchScope.defaultPlaceholder', { + defaultMessage: 'Type to find anything', + }), + [SearchScope.REPOSITORY]: i18n.translate('xpack.code.searchScope.repositoryPlaceholder', { + defaultMessage: 'Type to find repositories', + }), + [SearchScope.SYMBOL]: i18n.translate('xpack.code.searchScope.symbolPlaceholder', { + defaultMessage: 'Type to find symbols', + }), + [SearchScope.FILE]: i18n.translate('xpack.code.searchScope.filePlaceholder', { + defaultMessage: 'Type to find files', + }), }; export interface MainRouteParams { diff --git a/x-pack/legacy/plugins/code/public/components/admin_page/admin.tsx b/x-pack/legacy/plugins/code/public/components/admin_page/admin.tsx index bcaba5cc2d6e..bbdf92b9d54f 100644 --- a/x-pack/legacy/plugins/code/public/components/admin_page/admin.tsx +++ b/x-pack/legacy/plugins/code/public/components/admin_page/admin.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { parse as parseQuery } from 'querystring'; import React from 'react'; import { connect } from 'react-redux'; @@ -19,9 +20,8 @@ import { LanguageSeverTab } from './language_server_tab'; import { ProjectTab } from './project_tab'; enum AdminTabs { - projects = 'Repos', - roles = 'Roles', - languageServers = 'LanguageServers', + projects = '0', + languageServers = '1', } interface Props extends RouteComponentProps { @@ -56,12 +56,14 @@ class AdminPage extends React.PureComponent { public tabs = [ { id: AdminTabs.projects, - name: AdminTabs.projects, + name: i18n.translate('xpack.code.adminPage.repoTabLabel', { defaultMessage: 'Repositories' }), disabled: false, }, { id: AdminTabs.languageServers, - name: 'Language servers', + name: i18n.translate('xpack.code.adminPage.langserverTabLabel', { + defaultMessage: 'Language servers', + }), disabled: false, }, ]; diff --git a/x-pack/legacy/plugins/code/public/components/admin_page/empty_project.tsx b/x-pack/legacy/plugins/code/public/components/admin_page/empty_project.tsx index e34c932d6a2a..b574dc94805c 100644 --- a/x-pack/legacy/plugins/code/public/components/admin_page/empty_project.tsx +++ b/x-pack/legacy/plugins/code/public/components/admin_page/empty_project.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { capabilities } from 'ui/capabilities'; import { ImportProject } from './import_project'; @@ -19,15 +20,34 @@ export const EmptyProject = () => {
    -

    You don't have any repos yet

    +

    + +

    +
    + + {isAdmin && ( +

    + +

    + )}
    - {isAdmin &&

    Let's import your first one

    }
    {isAdmin && } - View the Setup Guide + + +
    diff --git a/x-pack/legacy/plugins/code/public/components/admin_page/import_project.tsx b/x-pack/legacy/plugins/code/public/components/admin_page/import_project.tsx index 6742ba95a663..3238d549faab 100644 --- a/x-pack/legacy/plugins/code/public/components/admin_page/import_project.tsx +++ b/x-pack/legacy/plugins/code/public/components/admin_page/import_project.tsx @@ -13,6 +13,9 @@ import { EuiGlobalToastList, EuiSpacer, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + import React, { ChangeEvent } from 'react'; import { connect } from 'react-redux'; import { closeToast, importRepo } from '../../actions'; @@ -71,11 +74,15 @@ class CodeImportProject extends React.PureComponent< - Import + diff --git a/x-pack/legacy/plugins/code/public/components/admin_page/language_server_tab.tsx b/x-pack/legacy/plugins/code/public/components/admin_page/language_server_tab.tsx index 44689603bc10..5c33aa513c83 100644 --- a/x-pack/legacy/plugins/code/public/components/admin_page/language_server_tab.tsx +++ b/x-pack/legacy/plugins/code/public/components/admin_page/language_server_tab.tsx @@ -20,6 +20,7 @@ import { EuiTabbedContent, EuiText, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { connect } from 'react-redux'; import { InstallationType } from '../../../common/installation'; @@ -51,24 +52,40 @@ const LanguageServerLi = (props: { let button = null; let state = null; if (status === LanguageServerStatus.RUNNING) { - state = Running ...; + state = ( + + + + ); } else if (status === LanguageServerStatus.NOT_INSTALLED) { state = ( - Not Installed + ); } else if (status === LanguageServerStatus.READY) { state = ( - Installed + ); } if (props.languageServer.installationType === InstallationType.Plugin) { button = ( - Setup + ); } @@ -139,12 +156,13 @@ class AdminLanguageSever extends React.PureComponent {

    - {this.props.languageServers.length} - {this.props.languageServers.length > 1 ? ( - Language servers - ) : ( - Language server - )} + + +

    @@ -186,16 +204,48 @@ const LanguageServerInstruction = (props: {
    -

    Install

    +

    + +

      -
    1. Stop your kibana Code node.
    2. -
    3. Use the following command to install the {props.name} language server.
    4. +
    5. + +
    6. +
    7. + +
    {installCode} -

    Uninstall

    +

    + +

      -
    1. Stop your kibana Code node.
    2. -
    3. Use the following command to remove the {props.name} language server.
    4. +
    5. + +
    6. +
    7. + +
    bin/kibana-plugin remove {props.pluginName} @@ -213,14 +263,22 @@ const LanguageServerInstruction = (props: { - Installation Instructions + + + - Close + diff --git a/x-pack/legacy/plugins/code/public/components/admin_page/project_item.tsx b/x-pack/legacy/plugins/code/public/components/admin_page/project_item.tsx index 62fbd6926992..c19c6c91f3ea 100644 --- a/x-pack/legacy/plugins/code/public/components/admin_page/project_item.tsx +++ b/x-pack/legacy/plugins/code/public/components/admin_page/project_item.tsx @@ -18,10 +18,12 @@ import { EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; import React from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { Repository, WorkerReservedProgress } from '../../../model'; import { deleteRepo, indexRepo, initRepoCommand } from '../../actions'; import { RepoState, RepoStatus } from '../../actions/status'; @@ -88,33 +90,70 @@ class CodeProjectItem extends React.PureComponent< let disableRepoLink = false; let hasError = false; if (!status) { - footer =
    INIT...
    ; + footer = ( +
    + +
    + ); } else if (status.state === RepoState.READY) { footer = (
    - LAST UPDATED: {moment(status.timestamp).fromNow()} + + :{' '} + {moment(status.timestamp) + .locale(i18n.getLocale()) + .fromNow()}
    ); } else if (status.state === RepoState.DELETING) { - footer =
    DELETING...
    ; + footer = ( +
    + +
    + ); } else if (status.state === RepoState.INDEXING) { footer = (
    - INDEXING... +
    ); } else if (status.state === RepoState.CLONING) { - footer =
    CLONING...
    ; + footer = ( +
    + +
    + ); } else if (status.state === RepoState.DELETE_ERROR) { - footer =
    ERROR DELETE REPO
    ; + footer = ( +
    + +
    + ); hasError = true; } else if (status.state === RepoState.INDEX_ERROR) { - footer =
    ERROR INDEX REPO
    ; + footer = ( +
    + +
    + ); hasError = true; } else if (status.state === RepoState.CLONE_ERROR) { footer = (
    - ERROR CLONING REPO  + +   @@ -161,7 +200,10 @@ class CodeProjectItem extends React.PureComponent< > - Settings +
    @@ -177,7 +219,10 @@ class CodeProjectItem extends React.PureComponent< > - Reindex +
    @@ -193,7 +238,10 @@ class CodeProjectItem extends React.PureComponent< > - Delete +
    @@ -244,11 +292,17 @@ class CodeProjectItem extends React.PureComponent< return ( @@ -259,11 +313,17 @@ class CodeProjectItem extends React.PureComponent< return ( diff --git a/x-pack/legacy/plugins/code/public/components/admin_page/project_tab.tsx b/x-pack/legacy/plugins/code/public/components/admin_page/project_tab.tsx index 5957cd4d5627..d5e0f4a52d12 100644 --- a/x-pack/legacy/plugins/code/public/components/admin_page/project_tab.tsx +++ b/x-pack/legacy/plugins/code/public/components/admin_page/project_tab.tsx @@ -24,6 +24,8 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React, { ChangeEvent } from 'react'; import { connect } from 'react-redux'; @@ -62,10 +64,36 @@ const sortFunctionsFactory = (status: { [key: string]: RepoStatus }) => { }; const sortOptions = [ - { value: SortOptionsValue.AlphabeticalAsc, inputDisplay: 'A to Z' }, - { value: SortOptionsValue.AlphabeticalDesc, inputDisplay: 'Z to A' }, - { value: SortOptionsValue.UpdatedAsc, inputDisplay: 'Last Updated ASC' }, - { value: SortOptionsValue.UpdatedDesc, inputDisplay: 'Last Updated DESC' }, + { + value: SortOptionsValue.AlphabeticalAsc, + inputDisplay: i18n.translate('xpack.code.adminPage.repoTab.sort.aToZDropDownOptionLabel', { + defaultMessage: 'A to Z', + }), + }, + { + value: SortOptionsValue.AlphabeticalDesc, + inputDisplay: i18n.translate('xpack.code.adminPage.repoTab.sort.zToADropDownOptionLabel', { + defaultMessage: 'Z to A', + }), + }, + { + value: SortOptionsValue.UpdatedAsc, + inputDisplay: i18n.translate( + 'xpack.code.adminPage.repoTab.sort.updatedAscDropDownOptionLabel', + { + defaultMessage: 'Last Updated ASC', + } + ), + }, + { + value: SortOptionsValue.UpdatedDesc, + inputDisplay: i18n.translate( + 'xpack.code.adminPage.repoTab.sort.updatedDescDropDownOptionLabel', + { + defaultMessage: 'Last Updated DESC', + } + ), + }, // { value: SortOptionsValue.recently_added, inputDisplay: 'Recently Added' }, ]; @@ -148,14 +176,29 @@ class CodeProjectTab extends React.PureComponent { - Import a new repo + + + -

    Repository URL

    +

    + +

    - + {
    - Cancel + + + - Import project +
    @@ -226,7 +277,11 @@ class CodeProjectTab extends React.PureComponent { - + { onClick={this.openModal} data-test-subj="newProjectButton" > - Import a new repo + )} @@ -252,8 +310,11 @@ class CodeProjectTab extends React.PureComponent {

    - {projectsCount} - {projectsCount === 1 ? Repo : Repos} +

    diff --git a/x-pack/legacy/plugins/code/public/components/admin_page/setup_guide.tsx b/x-pack/legacy/plugins/code/public/components/admin_page/setup_guide.tsx index ac13d54bdf1d..29d2cdad9a46 100644 --- a/x-pack/legacy/plugins/code/public/components/admin_page/setup_guide.tsx +++ b/x-pack/legacy/plugins/code/public/components/admin_page/setup_guide.tsx @@ -15,6 +15,8 @@ import { EuiTitle, EuiLink, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; @@ -23,80 +25,149 @@ import { RootState } from '../../reducers'; const steps = [ { - title: 'Check if multiple Kibana instances are used as a cluster', + title: i18n.translate('xpack.code.adminPage.setupGuide.checkMultiInstanceTitle', { + defaultMessage: 'Check if multiple Kibana instances are used as a clusterURL', + }), children: ( -

    If you are using single Kibana instance, you can skip this step.

    - If you are using multiple Kibana instances, you need to assign one Kibana instance as - `Code node`. To do this, add the following line of code into your kibana.yml file of every - Kibana instance and restart the instances: + +

    + +

    +

               xpack.code.codeNodeUrl: 'http://$YourCodeNodeAddress'
             

    - Where `$YourCodeNoteAddress` is the URL of your assigned Code node accessible by other - Kibana instances. +

    ), }, { - title: 'Install extra language support optionally', + title: i18n.translate('xpack.code.adminPage.setupGuide.installExtraLangSupportTitle', { + defaultMessage: 'Install extra language support optionally', + }), children: (

    - Look{' '} - - here - {' '} - to learn more about supported languages and language server installation. + + + + ), + }} + />

    - If you need Java language support, you can manage language server installation{' '} - here + + + + ), + }} + />

    ), }, { - title: 'Add a repository to Code', + title: i18n.translate('xpack.code.adminPage.setupGuide.addRepositoryTitle', { + defaultMessage: 'Add a repository to Code', + }), children: (

    - Import{' '} - - {' '} - a sample repo - {' '} - or{' '} - - your own repo - - . It is as easy as copy and paste git clone URLs to Code. + + + + ), + ownRepoLink: ( + + + + ), + }} + />

    ), }, { - title: 'Verify the repo is successfully imported', + title: i18n.translate('xpack.code.adminPage.setupGuide.verifyImportTitle', { + defaultMessage: 'Verify the repo is successfully imported', + }), children: (

    - You can verify your repo is successfully imported by{' '} - - searching - {' '} - and{' '} - - navigating - {' '} - the repo. If language support is available to the repo, make sure{' '} - - semantic navigation - {' '} - is available as well. + + + + ), + navigatingLink: ( + + + + ), + semanticNavigationLink: ( + + + + ), + }} + />

    ), @@ -106,11 +177,16 @@ const steps = [ const toastMessage = (

    - We’ve made some changes to roles and permissions in Kibana. Read more about how these changes - affect your Code implementation below.{' '} +

    - Learn more +
    ); @@ -133,7 +209,9 @@ class SetupGuidePage extends React.PureComponent<{ setupOk?: boolean }, { hideTo - - Back To project dashboard - + + + + + )} -

    Getting started in Elastic Code

    +

    + +

    diff --git a/x-pack/legacy/plugins/code/public/components/editor/editor.tsx b/x-pack/legacy/plugins/code/public/components/editor/editor.tsx index b186f3b7896d..9eb57e543386 100644 --- a/x-pack/legacy/plugins/code/public/components/editor/editor.tsx +++ b/x-pack/legacy/plugins/code/public/components/editor/editor.tsx @@ -30,6 +30,7 @@ export interface EditorActions { } interface Props { + hidden?: boolean; file?: FetchFileResponse; revealPosition?: Position; isReferencesOpen: boolean; @@ -45,6 +46,10 @@ interface Props { type IProps = Props & EditorActions & RouteComponentProps; export class EditorComponent extends React.Component { + static defaultProps = { + hidden: false, + }; + public blameWidgets: any; private container: HTMLElement | undefined; private monaco: MonacoHelper | undefined; @@ -152,7 +157,12 @@ export class EditorComponent extends React.Component { } public render() { return ( - +
    @@ -347,8 +358,19 @@ exports[`render full suggestions component 1`] = `
    - 1 - Result + + + 1 Result + +
    @@ -463,9 +485,19 @@ exports[`render full suggestions component 1`] = `
    - 2 - Result - s + + + 2 Results + +
    @@ -568,7 +600,16 @@ exports[`render full suggestions component 1`] = ` href="/search?q=string" onClick={[Function]} > - View More + + + + View More + +
    @@ -584,7 +625,15 @@ exports[`render full suggestions component 1`] = `
    - Press ⮐ Return for Full Text Search + + + Press ⮐ Return for Full Text Search + +
    diff --git a/x-pack/legacy/plugins/code/public/components/query_bar/components/typeahead/suggestions_component.tsx b/x-pack/legacy/plugins/code/public/components/query_bar/components/typeahead/suggestions_component.tsx index 0125aa0113cf..eed549b16546 100644 --- a/x-pack/legacy/plugins/code/public/components/query_bar/components/typeahead/suggestions_component.tsx +++ b/x-pack/legacy/plugins/code/public/components/query_bar/components/typeahead/suggestions_component.tsx @@ -5,6 +5,9 @@ */ import { EuiFlexGroup, EuiText, EuiToken, IconType } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + import { isEmpty } from 'lodash'; import React, { Component } from 'react'; import { Link } from 'react-router-dom'; @@ -53,7 +56,10 @@ export class SuggestionsComponent extends Component { {this.renderSuggestionGroups()}
    - Press ⮐ Return for Full Text Search +
    @@ -109,15 +115,24 @@ export class SuggestionsComponent extends Component {
    - {total} Result - {total === 1 ? '' : 's'} +
    ); const viewMore = (
    - View More + + {' '} + +
    ); @@ -153,11 +168,17 @@ export class SuggestionsComponent extends Component { private getGroupTitle(type: AutocompleteSuggestionType): string { switch (type) { case AutocompleteSuggestionType.FILE: - return 'Files'; + return i18n.translate('xpack.code.searchBar.fileGroupTitle', { + defaultMessage: 'Files', + }); case AutocompleteSuggestionType.REPOSITORY: - return 'Repos'; + return i18n.translate('xpack.code.searchBar.repositorylGroupTitle', { + defaultMessage: 'Repos', + }); case AutocompleteSuggestionType.SYMBOL: - return 'Symbols'; + return i18n.translate('xpack.code.searchBar.symbolGroupTitle', { + defaultMessage: 'Symbols', + }); } } diff --git a/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/file_suggestions_provider.ts b/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/file_suggestions_provider.ts index 2e49e769f963..873c22bb91f2 100644 --- a/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/file_suggestions_provider.ts +++ b/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/file_suggestions_provider.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { AbstractSuggestionsProvider, @@ -28,9 +28,7 @@ export class FileSuggestionsProvider extends AbstractSuggestionsProvider { if (repoScope && repoScope.length > 0) { queryParams.repoScope = repoScope.join(','); } - const res = await kfetch({ - pathname: `/api/code/suggestions/doc`, - method: 'get', + const res = await npStart.core.http.get(`/api/code/suggestions/doc`, { query: queryParams, }); const suggestions = Array.from(res.results as SearchResultItem[]) diff --git a/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/repository_suggestions_provider.ts b/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/repository_suggestions_provider.ts index 5c5a4129c1b4..4696c11e1b90 100644 --- a/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/repository_suggestions_provider.ts +++ b/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/repository_suggestions_provider.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { AbstractSuggestionsProvider, @@ -28,9 +28,7 @@ export class RepositorySuggestionsProvider extends AbstractSuggestionsProvider { if (repoScope && repoScope.length > 0) { queryParams.repoScope = repoScope.join(','); } - const res = await kfetch({ - pathname: `/api/code/suggestions/repo`, - method: 'get', + const res = await npStart.core.http.get(`/api/code/suggestions/repo`, { query: queryParams, }); const suggestions = Array.from(res.repositories as Repository[]) diff --git a/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/symbol_suggestions_provider.ts b/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/symbol_suggestions_provider.ts index 0d72e41921cf..0e321d6cf1ec 100644 --- a/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/symbol_suggestions_provider.ts +++ b/x-pack/legacy/plugins/code/public/components/query_bar/suggestions/symbol_suggestions_provider.ts @@ -5,7 +5,7 @@ */ import { DetailSymbolInformation } from '@elastic/lsp-extension'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { Location } from 'vscode-languageserver'; import { @@ -32,9 +32,7 @@ export class SymbolSuggestionsProvider extends AbstractSuggestionsProvider { if (repoScope && repoScope.length > 0) { queryParams.repoScope = repoScope.join(','); } - const res = await kfetch({ - pathname: `/api/code/suggestions/symbol`, - method: 'get', + const res = await npStart.core.http.get(`/api/code/suggestions/symbol`, { query: queryParams, }); const suggestions = Array.from(res.symbols as DetailSymbolInformation[]) diff --git a/x-pack/legacy/plugins/code/public/components/search_page/code_result.tsx b/x-pack/legacy/plugins/code/public/components/search_page/code_result.tsx index 82aea7a91d52..632c221f0680 100644 --- a/x-pack/legacy/plugins/code/public/components/search_page/code_result.tsx +++ b/x-pack/legacy/plugins/code/public/components/search_page/code_result.tsx @@ -5,6 +5,7 @@ */ import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { IPosition } from 'monaco-editor'; import React from 'react'; import { Link } from 'react-router-dom'; @@ -63,7 +64,10 @@ export class CodeResult extends React.PureComponent { {hits} -  hits from  + {filePath} diff --git a/x-pack/legacy/plugins/code/public/components/search_page/empty_placeholder.tsx b/x-pack/legacy/plugins/code/public/components/search_page/empty_placeholder.tsx index 4db575438dba..f5117d97471f 100644 --- a/x-pack/legacy/plugins/code/public/components/search_page/empty_placeholder.tsx +++ b/x-pack/legacy/plugins/code/public/components/search_page/empty_placeholder.tsx @@ -5,6 +5,7 @@ */ import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; export const EmptyPlaceholder = (props: any) => { @@ -25,11 +26,17 @@ export const EmptyPlaceholder = (props: any) => { - Hmmm... we looked for that, but couldn’t find anything. + - You can search for something else or modify your search settings. + @@ -41,7 +48,10 @@ export const EmptyPlaceholder = (props: any) => { } }} > - Modify your search settings +
    diff --git a/x-pack/legacy/plugins/code/public/components/search_page/scope_tabs.tsx b/x-pack/legacy/plugins/code/public/components/search_page/scope_tabs.tsx index cffa3e9e679a..8370400bdef8 100644 --- a/x-pack/legacy/plugins/code/public/components/search_page/scope_tabs.tsx +++ b/x-pack/legacy/plugins/code/public/components/search_page/scope_tabs.tsx @@ -5,6 +5,7 @@ */ import { EuiTab, EuiTabs } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import querystring from 'querystring'; import React from 'react'; import url from 'url'; @@ -44,14 +45,20 @@ export class ScopeTabs extends React.PureComponent { isSelected={this.props.scope !== SearchScope.REPOSITORY} onClick={this.onTabClicked(SearchScope.DEFAULT)} > - Code + - Repository +
    diff --git a/x-pack/legacy/plugins/code/public/components/search_page/search.tsx b/x-pack/legacy/plugins/code/public/components/search_page/search.tsx index c52769b28e10..effb4da4c7f9 100644 --- a/x-pack/legacy/plugins/code/public/components/search_page/search.tsx +++ b/x-pack/legacy/plugins/code/public/components/search_page/search.tsx @@ -5,6 +5,7 @@ */ import { EuiFlexItem, EuiLoadingSpinner, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import querystring from 'querystring'; import React from 'react'; import { connect } from 'react-redux'; @@ -166,7 +167,11 @@ class SearchPage extends React.PureComponent { const statsComp = (

    - Showing {total > 0 ? from : 0} - {to} of {total} results. +

    ); @@ -186,7 +191,11 @@ class SearchPage extends React.PureComponent { const statsComp = (

    - Showing {total > 0 ? from : 0} - {to} of {total} results. +

    ); diff --git a/x-pack/legacy/plugins/code/public/components/search_page/side_bar.tsx b/x-pack/legacy/plugins/code/public/components/search_page/side_bar.tsx index 617315e1e12b..7d263fbbf4a2 100644 --- a/x-pack/legacy/plugins/code/public/components/search_page/side_bar.tsx +++ b/x-pack/legacy/plugins/code/public/components/search_page/side_bar.tsx @@ -13,6 +13,7 @@ import { EuiTitle, EuiToken, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { RepositoryUtils } from '../../../common/repository_utils'; @@ -116,7 +117,12 @@ export class SideBar extends React.PureComponent { -

    Repositories

    +

    + +

    @@ -136,7 +142,12 @@ export class SideBar extends React.PureComponent { -

    Languages

    +

    + +

    diff --git a/x-pack/legacy/plugins/code/public/components/status_indicator/status_indicator.tsx b/x-pack/legacy/plugins/code/public/components/status_indicator/status_indicator.tsx index 5c8ab208eb58..4c4454c2003e 100644 --- a/x-pack/legacy/plugins/code/public/components/status_indicator/status_indicator.tsx +++ b/x-pack/legacy/plugins/code/public/components/status_indicator/status_indicator.tsx @@ -17,6 +17,7 @@ import { LangServerType, REPO_FILE_STATUS_SEVERITY, RepoFileStatus, + RepoFileStatusText as StatusText, Severity, StatusReport, } from '../../../common/repo_file_status'; @@ -63,7 +64,6 @@ export class StatusIndicatorComponent extends React.Component { const { statusReport } = this.props; let severity = Severity.NONE; const children: any[] = []; - const addError = (error: RepoFileStatus | LangServerType) => { // @ts-ignore const s: any = REPO_FILE_STATUS_SEVERITY[error]; @@ -80,7 +80,7 @@ export class StatusIndicatorComponent extends React.Component {

    ); } else { - children.push(

    {error}

    ); + children.push(

    {StatusText[error]}

    ); } } }; diff --git a/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/__fixtures__/props.ts b/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/__fixtures__/props.ts index 17bac61320d7..165957553d5d 100644 --- a/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/__fixtures__/props.ts +++ b/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/__fixtures__/props.ts @@ -7,107 +7,86 @@ import { SymbolKind } from 'vscode-languageserver-types'; import { SymbolWithMembers } from '../../../../actions/structure'; -export const props: { structureTree: SymbolWithMembers[] } = { +export const props: { structureTree: SymbolWithMembers[]; uri: string } = { + uri: + 'git://github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', structureTree: [ { name: '"stack-control"', kind: SymbolKind.Module, - location: { - uri: - 'git://github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', - range: { start: { line: 0, character: 0 }, end: { line: 27, character: 0 } }, - }, + range: { start: { line: 0, character: 0 }, end: { line: 27, character: 0 } }, + selectionRange: { start: { line: 0, character: 0 }, end: { line: 27, character: 0 } }, path: '"stack-control"', members: [ { name: 'EventEmitter', kind: SymbolKind.Variable, - location: { - uri: - 'git://github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', - range: { start: { line: 9, character: 9 }, end: { line: 9, character: 21 } }, - }, - containerName: '"stack-control"', + range: { start: { line: 9, character: 9 }, end: { line: 9, character: 21 } }, + selectionRange: { start: { line: 9, character: 9 }, end: { line: 9, character: 21 } }, path: '"stack-control"/EventEmitter', }, { name: 'ClrStackView', kind: SymbolKind.Variable, - location: { - uri: - 'git://github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', - range: { start: { line: 10, character: 9 }, end: { line: 10, character: 21 } }, - }, - containerName: '"stack-control"', + range: { start: { line: 10, character: 9 }, end: { line: 10, character: 21 } }, + selectionRange: { start: { line: 10, character: 9 }, end: { line: 10, character: 21 } }, path: '"stack-control"/ClrStackView', }, { name: 'StackControl', kind: SymbolKind.Class, - location: { - uri: - 'git://github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', - range: { start: { line: 12, character: 0 }, end: { line: 26, character: 1 } }, - }, - containerName: '"stack-control"', + range: { start: { line: 12, character: 0 }, end: { line: 26, character: 1 } }, + selectionRange: { start: { line: 12, character: 0 }, end: { line: 26, character: 1 } }, path: '"stack-control"/StackControl', members: [ { name: 'model', kind: SymbolKind.Property, - location: { - uri: - 'git://github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', - range: { start: { line: 13, character: 2 }, end: { line: 13, character: 13 } }, + range: { start: { line: 13, character: 2 }, end: { line: 13, character: 13 } }, + selectionRange: { + start: { line: 13, character: 2 }, + end: { line: 13, character: 13 }, }, - containerName: 'StackControl', path: '"stack-control"/StackControl/model', }, { name: 'modelChange', kind: SymbolKind.Property, - location: { - uri: - 'git://github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', - range: { start: { line: 14, character: 2 }, end: { line: 14, character: 64 } }, + range: { start: { line: 14, character: 2 }, end: { line: 14, character: 64 } }, + selectionRange: { + start: { line: 14, character: 2 }, + end: { line: 14, character: 64 }, }, - containerName: 'StackControl', path: '"stack-control"/StackControl/modelChange', }, { name: 'stackView', kind: SymbolKind.Property, - location: { - uri: - 'git://github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', - range: { start: { line: 16, character: 14 }, end: { line: 16, character: 47 } }, + range: { start: { line: 16, character: 14 }, end: { line: 16, character: 47 } }, + selectionRange: { + start: { line: 16, character: 14 }, + end: { line: 16, character: 47 }, }, - containerName: 'StackControl', path: '"stack-control"/StackControl/stackView', }, { name: 'HashMap', kind: SymbolKind.Class, - location: { - uri: - 'git://github.com/elastic/openjdkMirror/blob/master/jdk/src/share/classes/java/util/HashMap.java', - range: { start: { line: 136, character: 13 }, end: { line: 136, character: 20 } }, + range: { start: { line: 136, character: 13 }, end: { line: 136, character: 20 } }, + selectionRange: { + start: { line: 136, character: 13 }, + end: { line: 136, character: 20 }, }, - containerName: 'HashMap.java', path: 'HashMap', members: [ { name: 'serialVersionUID', kind: SymbolKind.Field, - location: { - uri: - 'git://github.com/elastic/openjdkMirror/blob/master/jdk/src/share/classes/java/util/HashMap.java', - range: { - start: { line: 139, character: 30 }, - end: { line: 139, character: 46 }, - }, + range: { start: { line: 139, character: 30 }, end: { line: 139, character: 46 } }, + selectionRange: { + start: { line: 139, character: 30 }, + end: { line: 139, character: 46 }, }, - containerName: 'HashMap', path: 'HashMap/serialVersionUID', }, ], @@ -115,20 +94,20 @@ export const props: { structureTree: SymbolWithMembers[] } = { { name: 'Unit', kind: SymbolKind.Variable, - location: { - uri: - 'git://github.com/elastic/kibana/blob/master/packages/elastic-datemath/src/index.d.ts', - range: { start: { line: 20, character: 0 }, end: { line: 20, character: 66 } }, + range: { start: { line: 20, character: 0 }, end: { line: 20, character: 66 } }, + selectionRange: { + start: { line: 20, character: 0 }, + end: { line: 20, character: 66 }, }, path: 'Unit', }, { name: 'datemath', kind: SymbolKind.Constant, - location: { - uri: - 'git://github.com/elastic/kibana/blob/master/packages/elastic-datemath/src/index.d.ts', - range: { start: { line: 22, character: 14 }, end: { line: 47, character: 1 } }, + range: { start: { line: 22, character: 14 }, end: { line: 47, character: 1 } }, + selectionRange: { + start: { line: 22, character: 14 }, + end: { line: 47, character: 1 }, }, path: 'datemath', }, diff --git a/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/__snapshots__/symbol_tree.test.tsx.snap b/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/__snapshots__/symbol_tree.test.tsx.snap index 445bbb65adda..d079094cf21c 100644 --- a/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/__snapshots__/symbol_tree.test.tsx.snap +++ b/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/__snapshots__/symbol_tree.test.tsx.snap @@ -375,24 +375,34 @@ exports[`render symbol tree correctly 1`] = `
    - datemath + Unit
    @@ -457,34 +512,24 @@ exports[`render symbol tree correctly 1`] = `
    diff --git a/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/symbol_tree.test.tsx b/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/symbol_tree.test.tsx index 27d7188e8911..47a265512bfb 100644 --- a/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/symbol_tree.test.tsx +++ b/x-pack/legacy/plugins/code/public/components/symbol_tree/__test__/symbol_tree.test.tsx @@ -19,20 +19,21 @@ import { MainRouteParams, PathTypes } from '../../../common/types'; import { History, Location } from 'history'; const location: Location = createLocation({ - pathname: '/github.com/google/guava/tree/master/guava/src/com/google', + pathname: + '/github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', }); const m: match = createMatch({ path: '/:resource/:org/:repo/:pathType(blob|tree)/:revision/:path*:goto(!.*)?', - url: '/github.com/google/guava/tree/master/guava/src/com/google', + url: '/github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts', isExact: true, params: { resource: 'github.com', org: 'google', - repo: 'guava', - pathType: PathTypes.tree, + repo: 'vmware', + pathType: PathTypes.blob, revision: 'master', - path: 'guava/src/com/google', + path: 'src/clr-angular/data/stack-view/stack-control.ts', }, }); @@ -50,6 +51,7 @@ test('render symbol tree correctly', () => { closedPaths={[]} openSymbolPath={mockFunction} closeSymbolPath={mockFunction} + uri="git://github.com/vmware/clarity/blob/master/src/clr-angular/data/stack-view/stack-control.ts" /> ) diff --git a/x-pack/legacy/plugins/code/public/components/symbol_tree/code_symbol_tree.tsx b/x-pack/legacy/plugins/code/public/components/symbol_tree/code_symbol_tree.tsx index 57e0b4b27d06..3d189c40a0b1 100644 --- a/x-pack/legacy/plugins/code/public/components/symbol_tree/code_symbol_tree.tsx +++ b/x-pack/legacy/plugins/code/public/components/symbol_tree/code_symbol_tree.tsx @@ -9,7 +9,7 @@ import { IconType } from '@elastic/eui'; import React from 'react'; import { Link, RouteComponentProps } from 'react-router-dom'; import url from 'url'; -import { Location, SymbolKind } from 'vscode-languageserver-types/lib/umd/main'; +import { Range, SymbolKind } from 'vscode-languageserver-types'; import { isEqual } from 'lodash'; import { RepositoryUtils } from '../../../common/repository_utils'; @@ -21,20 +21,12 @@ interface Props extends RouteComponentProps { closedPaths: string[]; openSymbolPath: (p: string) => void; closeSymbolPath: (p: string) => void; + uri: string; } -const sortSymbol = (a: SymbolWithMembers, b: SymbolWithMembers) => { - const lineDiff = a.location.range.start.line - b.location.range.start.line; - if (lineDiff === 0) { - return a.location.range.start.character - b.location.range.start.character; - } else { - return lineDiff; - } -}; - interface ActiveSymbol { name: string; - location: Location; + range: Range; } export class CodeSymbolTree extends React.PureComponent { @@ -45,7 +37,7 @@ export class CodeSymbolTree extends React.PureComponent; } @@ -89,11 +81,11 @@ export class CodeSymbolTree extends React.PureComponent @@ -108,7 +100,7 @@ export class CodeSymbolTree extends React.PureComponent { - return symbolsWithMembers.sort(sortSymbol).map((s: SymbolWithMembers, index: number) => { + return symbolsWithMembers.map((s: SymbolWithMembers, index: number) => { const item: EuiSideNavItem = { name: s.name, id: `${s.name}_${index}`, @@ -120,16 +112,16 @@ export class CodeSymbolTree extends React.PureComponent 0, item.forceOpen, s.path ); } else { item.renderItem = this.getStructureTreeItemRenderer( - s.location, + s.range, s.name, s.kind, false, diff --git a/x-pack/legacy/plugins/code/public/index.ts b/x-pack/legacy/plugins/code/public/index.ts new file mode 100644 index 000000000000..ddf717779634 --- /dev/null +++ b/x-pack/legacy/plugins/code/public/index.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 { PluginInitializerContext } from 'src/core/public'; +import { npStart } from 'ui/new_platform'; +import { Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} + +// This is the shim to legacy platform +const p = plugin({} as PluginInitializerContext); +p.start(npStart.core); diff --git a/x-pack/legacy/plugins/code/public/lib/documentation_links.ts b/x-pack/legacy/plugins/code/public/lib/documentation_links.ts index 53da0b46518b..f64788b1518d 100644 --- a/x-pack/legacy/plugins/code/public/lib/documentation_links.ts +++ b/x-pack/legacy/plugins/code/public/lib/documentation_links.ts @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { npStart } from 'ui/new_platform'; // TODO make sure document links are right export const documentationLinks = { - code: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code.html`, - codeIntelligence: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code.html`, - gitFormat: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code.html`, - codeInstallLangServer: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-install-lang-server.html`, - codeGettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-getting-started.html`, - codeRepoManagement: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-repo-management.html`, - codeSearch: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-search.html`, - codeOtherFeatures: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-basic-nav.html`, - semanticNavigation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-semantic-nav.html`, - kibanaRoleManagement: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-role-management.html`, + code: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/code.html`, + codeIntelligence: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/code.html`, + gitFormat: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/code.html`, + codeInstallLangServer: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/code-install-lang-server.html`, + codeGettingStarted: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/code-getting-started.html`, + codeRepoManagement: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/code-repo-management.html`, + codeSearch: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/code-search.html`, + codeOtherFeatures: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/code-basic-nav.html`, + semanticNavigation: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/code-semantic-nav.html`, + kibanaRoleManagement: `${npStart.core.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${npStart.core.docLinks.DOC_LINK_VERSION}/kibana-role-management.html`, }; diff --git a/x-pack/legacy/plugins/code/public/monaco/definition/definition_provider.ts b/x-pack/legacy/plugins/code/public/monaco/definition/definition_provider.ts index 0807195549fe..f03c914be3f3 100644 --- a/x-pack/legacy/plugins/code/public/monaco/definition/definition_provider.ts +++ b/x-pack/legacy/plugins/code/public/monaco/definition/definition_provider.ts @@ -6,7 +6,7 @@ import { DetailSymbolInformation } from '@elastic/lsp-extension'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { Location } from 'vscode-languageserver-types'; import { monaco } from '../monaco'; import { LspRestClient, TextDocumentMethods } from '../../../common/lsp_client'; @@ -32,7 +32,7 @@ export const definitionProvider: monaco.languages.DefinitionProvider = { } async function handleQname(qname: string): Promise { - const res: any = await kfetch({ pathname: `/api/code/lsp/symbol/${qname}` }); + const res = await npStart.core.http.get(`/api/code/lsp/symbol/${qname}`); if (res.symbols) { return res.symbols.map((s: DetailSymbolInformation) => handleLocation(s.symbolInformation.location) diff --git a/x-pack/legacy/plugins/code/public/monaco/editor_service.ts b/x-pack/legacy/plugins/code/public/monaco/editor_service.ts index f71c5df7efe2..2b3b5513a39a 100644 --- a/x-pack/legacy/plugins/code/public/monaco/editor_service.ts +++ b/x-pack/legacy/plugins/code/public/monaco/editor_service.ts @@ -7,7 +7,7 @@ import { editor, IRange, Uri } from 'monaco-editor'; // @ts-ignore import { StandaloneCodeEditorServiceImpl } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneCodeServiceImpl.js'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { parseSchema } from '../../common/uri_util'; import { SymbolSearchResult } from '../../model'; import { history } from '../utils/url'; @@ -37,10 +37,7 @@ export class EditorService extends StandaloneCodeEditorServiceImpl { public static async findSymbolByQname(qname: string) { try { - const response = await kfetch({ - pathname: `/api/code/lsp/symbol/${qname}`, - method: 'GET', - }); + const response = await npStart.core.http.get(`/api/code/lsp/symbol/${qname}`); return response as SymbolSearchResult; } catch (e) { const error = e.body; diff --git a/x-pack/legacy/plugins/code/public/monaco/hover/content_hover_widget.ts b/x-pack/legacy/plugins/code/public/monaco/hover/content_hover_widget.ts deleted file mode 100644 index 987c7d947740..000000000000 --- a/x-pack/legacy/plugins/code/public/monaco/hover/content_hover_widget.ts +++ /dev/null @@ -1,208 +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 { editor as Editor, languages, Range as EditorRange } from 'monaco-editor'; -// @ts-ignore -import { createCancelablePromise } from 'monaco-editor/esm/vs/base/common/async'; -// @ts-ignore -import { getOccurrencesAtPosition } from 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/wordHighlighter'; - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Hover, MarkedString, Range } from 'vscode-languageserver-types'; -import { ServerNotInitialized } from '../../../common/lsp_error_codes'; -import { HoverButtons } from '../../components/hover/hover_buttons'; -import { HoverState, HoverWidget, HoverWidgetProps } from '../../components/hover/hover_widget'; -import { ContentWidget } from '../content_widget'; -import { monaco } from '../monaco'; -import { Operation } from '../operation'; -import { HoverComputer } from './hover_computer'; - -export class ContentHoverWidget extends ContentWidget { - public static ID = 'editor.contrib.contentHoverWidget'; - private static readonly DECORATION_OPTIONS = { - className: 'wordHighlightStrong', // hoverHighlight wordHighlightStrong - }; - private hoverOperation: Operation; - private readonly computer: HoverComputer; - private lastRange: EditorRange | null = null; - private shouldFocus: boolean = false; - private hoverResultAction?: (hover: Hover) => void = undefined; - private highlightDecorations: string[] = []; - private hoverState: HoverState = HoverState.LOADING; - - constructor(editor: Editor.ICodeEditor) { - super(ContentHoverWidget.ID, editor); - this.containerDomNode.className = 'monaco-editor-hover hidden'; - this.containerDomNode.tabIndex = 0; - this.domNode.className = 'monaco-editor-hover-content'; - this.computer = new HoverComputer(); - this.hoverOperation = new Operation( - this.computer, - result => this.result(result), - error => { - // @ts-ignore - if (error.code === ServerNotInitialized) { - this.hoverState = HoverState.INITIALIZING; - this.render(this.lastRange!); - } - }, - () => { - this.hoverState = HoverState.LOADING; - this.render(this.lastRange!); - } - ); - } - - public startShowingAt(range: any, focus: boolean) { - if (this.isVisible && this.lastRange && this.lastRange.containsRange(range)) { - return; - } - this.hoverOperation.cancel(); - const url = this.editor.getModel()!.uri.toString(); - if (this.isVisible) { - this.hide(); - } - this.computer.setParams(url, range); - this.hoverOperation.start(); - this.lastRange = range; - this.shouldFocus = focus; - } - - public setHoverResultAction(hoverResultAction: (hover: Hover) => void) { - this.hoverResultAction = hoverResultAction; - } - - public hide(): void { - super.hide(); - this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, []); - } - - private result(result: Hover) { - if (this.hoverResultAction) { - // pass the result to redux - this.hoverResultAction(result); - } - if (this.lastRange && result && result.contents) { - this.render(this.lastRange, result); - } else { - this.hide(); - } - } - - private render(renderRange: EditorRange, result?: Hover) { - const fragment = document.createDocumentFragment(); - let props: HoverWidgetProps = { - state: this.hoverState, - gotoDefinition: this.gotoDefinition.bind(this), - findReferences: this.findReferences.bind(this), - }; - let startColumn = renderRange.startColumn; - if (result) { - let contents: MarkedString[] = []; - if (Array.isArray(result.contents)) { - contents = result.contents; - } else { - contents = [result.contents as MarkedString]; - } - contents = contents.filter(v => { - if (typeof v === 'string') { - return !!v; - } else { - return !!v.value; - } - }); - if (contents.length === 0) { - this.hide(); - return; - } - props = { - ...props, - state: HoverState.READY, - contents, - }; - if (result.range) { - this.lastRange = this.toMonacoRange(result.range); - this.highlightOccurrences(this.lastRange); - } - startColumn = Math.min( - renderRange.startColumn, - result.range ? result.range.start.character + 1 : Number.MAX_VALUE - ); - } - - this.showAt(new monaco.Position(renderRange.startLineNumber, startColumn), this.shouldFocus); - const element = React.createElement(HoverWidget, props, null); - // @ts-ignore - ReactDOM.render(element, fragment); - const buttonFragment = document.createDocumentFragment(); - const buttons = React.createElement(HoverButtons, props, null); - // @ts-ignore - ReactDOM.render(buttons, buttonFragment); - this.updateContents(fragment, buttonFragment); - } - - private toMonacoRange(r: Range): EditorRange { - return new monaco.Range( - r.start.line + 1, - r.start.character + 1, - r.end.line + 1, - r.end.character + 1 - ); - } - - private gotoDefinition() { - if (this.lastRange) { - this.editor.setPosition({ - lineNumber: this.lastRange.startLineNumber, - column: this.lastRange.startColumn, - }); - const action = this.editor.getAction('editor.action.revealDefinition'); - action.run().then(() => this.hide()); - } - } - - private findReferences() { - if (this.lastRange) { - this.editor.setPosition({ - lineNumber: this.lastRange.startLineNumber, - column: this.lastRange.startColumn, - }); - const action = this.editor.getAction('editor.action.referenceSearch.trigger'); - action.run().then(() => this.hide()); - } - } - - private highlightOccurrences(range: EditorRange) { - const pos = new monaco.Position(range.startLineNumber, range.startColumn); - return createCancelablePromise((token: any) => - getOccurrencesAtPosition(this.editor.getModel(), pos, token).then( - (data: languages.DocumentHighlight[]) => { - if (data) { - if (this.isVisible) { - const decorations = data.map(h => ({ - range: h.range, - options: ContentHoverWidget.DECORATION_OPTIONS, - })); - - this.highlightDecorations = this.editor.deltaDecorations( - this.highlightDecorations, - decorations - ); - } - } else { - this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [ - { - range, - options: ContentHoverWidget.DECORATION_OPTIONS, - }, - ]); - } - } - ) - ); - } -} diff --git a/x-pack/legacy/plugins/code/public/monaco/hover/content_hover_widget.tsx b/x-pack/legacy/plugins/code/public/monaco/hover/content_hover_widget.tsx new file mode 100644 index 000000000000..99556fa1d2b7 --- /dev/null +++ b/x-pack/legacy/plugins/code/public/monaco/hover/content_hover_widget.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { I18nProvider } from '@kbn/i18n/react'; +import { editor as Editor, languages, Range as EditorRange } from 'monaco-editor'; +// @ts-ignore +import { createCancelablePromise } from 'monaco-editor/esm/vs/base/common/async'; +// @ts-ignore +import { getOccurrencesAtPosition } from 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/wordHighlighter'; + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Hover, MarkedString, Range } from 'vscode-languageserver-types'; +import { ServerNotInitialized } from '../../../common/lsp_error_codes'; +import { HoverButtons } from '../../components/hover/hover_buttons'; +import { HoverState, HoverWidget, HoverWidgetProps } from '../../components/hover/hover_widget'; +import { ContentWidget } from '../content_widget'; +import { monaco } from '../monaco'; +import { Operation } from '../operation'; +import { HoverComputer } from './hover_computer'; + +export class ContentHoverWidget extends ContentWidget { + public static ID = 'editor.contrib.contentHoverWidget'; + private static readonly DECORATION_OPTIONS = { + className: 'wordHighlightStrong', // hoverHighlight wordHighlightStrong + }; + private hoverOperation: Operation; + private readonly computer: HoverComputer; + private lastRange: EditorRange | null = null; + private shouldFocus: boolean = false; + private hoverResultAction?: (hover: Hover) => void = undefined; + private highlightDecorations: string[] = []; + private hoverState: HoverState = HoverState.LOADING; + + constructor(editor: Editor.ICodeEditor) { + super(ContentHoverWidget.ID, editor); + this.containerDomNode.className = 'monaco-editor-hover hidden'; + this.containerDomNode.tabIndex = 0; + this.domNode.className = 'monaco-editor-hover-content'; + this.computer = new HoverComputer(); + this.hoverOperation = new Operation( + this.computer, + result => this.result(result), + error => { + // @ts-ignore + if (error.code === ServerNotInitialized) { + this.hoverState = HoverState.INITIALIZING; + this.render(this.lastRange!); + } + }, + () => { + this.hoverState = HoverState.LOADING; + this.render(this.lastRange!); + } + ); + } + + public startShowingAt(range: any, focus: boolean) { + if (this.isVisible && this.lastRange && this.lastRange.containsRange(range)) { + return; + } + this.hoverOperation.cancel(); + const url = this.editor.getModel()!.uri.toString(); + if (this.isVisible) { + this.hide(); + } + this.computer.setParams(url, range); + this.hoverOperation.start(); + this.lastRange = range; + this.shouldFocus = focus; + } + + public setHoverResultAction(hoverResultAction: (hover: Hover) => void) { + this.hoverResultAction = hoverResultAction; + } + + public hide(): void { + super.hide(); + this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, []); + } + + private result(result: Hover) { + if (this.hoverResultAction) { + // pass the result to redux + this.hoverResultAction(result); + } + if (this.lastRange && result && result.contents) { + this.render(this.lastRange, result); + } else { + this.hide(); + } + } + + private render(renderRange: EditorRange, result?: Hover) { + const fragment = document.createDocumentFragment(); + let props: HoverWidgetProps = { + state: this.hoverState, + gotoDefinition: this.gotoDefinition.bind(this), + findReferences: this.findReferences.bind(this), + }; + let startColumn = renderRange.startColumn; + if (result) { + let contents: MarkedString[] = []; + if (Array.isArray(result.contents)) { + contents = result.contents; + } else { + contents = [result.contents as MarkedString]; + } + contents = contents.filter(v => { + if (typeof v === 'string') { + return !!v; + } else { + return !!v.value; + } + }); + if (contents.length === 0) { + this.hide(); + return; + } + props = { + ...props, + state: HoverState.READY, + contents, + }; + if (result.range) { + this.lastRange = this.toMonacoRange(result.range); + this.highlightOccurrences(this.lastRange); + } + startColumn = Math.min( + renderRange.startColumn, + result.range ? result.range.start.character + 1 : Number.MAX_VALUE + ); + } + + this.showAt(new monaco.Position(renderRange.startLineNumber, startColumn), this.shouldFocus); + + const element = ( + + + + ); + // @ts-ignore + ReactDOM.render(element, fragment); + const buttonFragment = document.createDocumentFragment(); + const buttons = ( + + + + ); + // @ts-ignore + ReactDOM.render(buttons, buttonFragment); + this.updateContents(fragment, buttonFragment); + } + + private toMonacoRange(r: Range): EditorRange { + return new monaco.Range( + r.start.line + 1, + r.start.character + 1, + r.end.line + 1, + r.end.character + 1 + ); + } + + private gotoDefinition() { + if (this.lastRange) { + this.editor.setPosition({ + lineNumber: this.lastRange.startLineNumber, + column: this.lastRange.startColumn, + }); + const action = this.editor.getAction('editor.action.revealDefinition'); + action.run().then(() => this.hide()); + } + } + + private findReferences() { + if (this.lastRange) { + this.editor.setPosition({ + lineNumber: this.lastRange.startLineNumber, + column: this.lastRange.startColumn, + }); + const action = this.editor.getAction('editor.action.referenceSearch.trigger'); + action.run().then(() => this.hide()); + } + } + + private highlightOccurrences(range: EditorRange) { + const pos = new monaco.Position(range.startLineNumber, range.startColumn); + return createCancelablePromise((token: any) => + getOccurrencesAtPosition(this.editor.getModel(), pos, token).then( + (data: languages.DocumentHighlight[]) => { + if (data) { + if (this.isVisible) { + const decorations = data.map(h => ({ + range: h.range, + options: ContentHoverWidget.DECORATION_OPTIONS, + })); + + this.highlightDecorations = this.editor.deltaDecorations( + this.highlightDecorations, + decorations + ); + } + } else { + this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [ + { + range, + options: ContentHoverWidget.DECORATION_OPTIONS, + }, + ]); + } + } + ) + ); + } +} diff --git a/x-pack/legacy/plugins/code/public/monaco/monaco.ts b/x-pack/legacy/plugins/code/public/monaco/monaco.ts index ca8bea423175..06b55e4d74c5 100644 --- a/x-pack/legacy/plugins/code/public/monaco/monaco.ts +++ b/x-pack/legacy/plugins/code/public/monaco/monaco.ts @@ -96,11 +96,12 @@ import 'monaco-editor/esm/vs/basic-languages/powershell/powershell.contribution. import 'monaco-editor/esm/vs/basic-languages/python/python.contribution.js'; import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution'; import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { CTAGS_SUPPORT_LANGS } from '../../common/language_server'; import { definitionProvider } from './definition/definition_provider'; -const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode'); +const IS_DARK_THEME = npStart.core.uiSettings.get('theme:darkMode'); const themeName = IS_DARK_THEME ? darkTheme : lightTheme; diff --git a/x-pack/legacy/plugins/code/public/monaco/textmodel_resolver.ts b/x-pack/legacy/plugins/code/public/monaco/textmodel_resolver.ts index e33339bca56f..06fae9b59154 100644 --- a/x-pack/legacy/plugins/code/public/monaco/textmodel_resolver.ts +++ b/x-pack/legacy/plugins/code/public/monaco/textmodel_resolver.ts @@ -5,7 +5,7 @@ */ import { editor, IDisposable, Uri } from 'monaco-editor'; -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { ImmortalReference } from './immortal_reference'; @@ -55,7 +55,7 @@ export class TextModelResolverService implements ITextModelService { const revision = resource.query; const file = resource.fragment; const response = await fetch( - chrome.addBasePath(`/api/code/repo/${repo}/blob/${revision}/${file}`) + npStart.core.http.basePath.prepend(`/api/code/repo/${repo}/blob/${revision}/${file}`) ); if (response.status === 200) { const contentType = response.headers.get('Content-Type'); diff --git a/x-pack/legacy/plugins/code/public/plugin.ts b/x-pack/legacy/plugins/code/public/plugin.ts new file mode 100644 index 000000000000..48b295ef67d9 --- /dev/null +++ b/x-pack/legacy/plugins/code/public/plugin.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext, CoreSetup, CoreStart } from 'src/core/public'; +import { startApp } from './app'; + +export class Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup) { + // called when plugin is setting up + } + + public start(core: CoreStart) { + // called after all plugins are set up + startApp(core); + } + + public stop() { + // called when plugin is torn down, aka window.onbeforeunload + } +} diff --git a/x-pack/legacy/plugins/code/public/reducers/__tests__/match_container_name.test.ts b/x-pack/legacy/plugins/code/public/reducers/__tests__/match_container_name.test.ts deleted file mode 100644 index efc4be14286b..000000000000 --- a/x-pack/legacy/plugins/code/public/reducers/__tests__/match_container_name.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { matchContainerName } from '../../utils/symbol_utils'; - -describe('matchSymbolName', () => { - it('should match symbol whose name is exactly the container name', () => { - expect(matchContainerName('Session', 'Session')).toBe(true); - }); - it('should match symbol that has type annotation', () => { - expect(matchContainerName('Session', 'Session')).toBe(true); - }); - it('should not match symbol that has same start but not end with type annotation', () => { - expect(matchContainerName('Session', 'SessionTail')).toBe(false); - }); -}); diff --git a/x-pack/legacy/plugins/code/public/reducers/file.ts b/x-pack/legacy/plugins/code/public/reducers/file.ts index c97984e6c809..59bdaee28812 100644 --- a/x-pack/legacy/plugins/code/public/reducers/file.ts +++ b/x-pack/legacy/plugins/code/public/reducers/file.ts @@ -12,40 +12,52 @@ import { fetchFileSuccess, routeChange, setNotFound, + fetchFile, + FetchFilePayload, } from '../actions'; import { routePathChange, repoChange, revisionChange, filePathChange } from '../actions/route'; export interface FileState { file?: FetchFileResponse; isNotFound: boolean; + loading: boolean; } const initialState: FileState = { isNotFound: false, + loading: false, }; const clearState = (state: FileState) => produce(state, draft => { draft.file = undefined; draft.isNotFound = initialState.isNotFound; + draft.loading = initialState.loading; }); -type FilePayload = FetchFileResponse & boolean; +type FilePayload = FetchFileResponse & boolean & FetchFilePayload; export const file = handleActions( { + [String(fetchFile)]: (state, action: Action) => + produce(state, draft => { + draft.loading = true; + }), [String(fetchFileSuccess)]: (state, action: Action) => produce(state, draft => { draft.file = action.payload; draft.isNotFound = false; + draft.loading = false; }), [String(fetchFileFailed)]: state => produce(state, draft => { draft.file = undefined; + draft.loading = false; }), [String(setNotFound)]: (state, action: Action) => produce(state, draft => { draft.isNotFound = action.payload!; + draft.loading = false; }), [String(routeChange)]: state => produce(state, draft => { diff --git a/x-pack/legacy/plugins/code/public/reducers/file_tree.ts b/x-pack/legacy/plugins/code/public/reducers/file_tree.ts index 99c1883e772c..467f04f8f22a 100644 --- a/x-pack/legacy/plugins/code/public/reducers/file_tree.ts +++ b/x-pack/legacy/plugins/code/public/reducers/file_tree.ts @@ -8,13 +8,10 @@ import produce from 'immer'; import { Action, handleActions } from 'redux-actions'; import { FileTree, FileTreeItemType, sortFileTree } from '../../model'; import { - closeTreePath, fetchRepoTree, fetchRepoTreeFailed, fetchRepoTreeSuccess, - openTreePath, RepoTreePayload, - resetRepoTree, fetchRootRepoTreeSuccess, fetchRootRepoTreeFailed, dirNotFound, @@ -60,7 +57,6 @@ export function getPathOfTree(tree: FileTree, paths: string[]) { export interface FileTreeState { tree: FileTree; - openedPaths: string[]; fileTreeLoadingPaths: string[]; // store not found directory as an array to calculate `notFound` flag by finding whether path is in this array notFoundDirs: string[]; @@ -73,7 +69,6 @@ const initialState: FileTreeState = { path: '', type: FileTreeItemType.Directory, }, - openedPaths: [], fileTreeLoadingPaths: [''], notFoundDirs: [], revision: '', @@ -82,7 +77,6 @@ const initialState: FileTreeState = { const clearState = (state: FileTreeState) => produce(state, draft => { draft.tree = initialState.tree; - draft.openedPaths = initialState.openedPaths; draft.fileTreeLoadingPaths = initialState.fileTreeLoadingPaths; draft.notFoundDirs = initialState.notFoundDirs; draft.revision = initialState.revision; @@ -138,37 +132,12 @@ export const fileTree = handleActions( produce(state, draft => { draft.notFoundDirs.push(action.payload!); }), - [String(resetRepoTree)]: state => - produce(state, draft => { - draft.tree = initialState.tree; - draft.openedPaths = initialState.openedPaths; - }), [String(fetchRepoTreeFailed)]: (state, action: Action) => produce(state, draft => { draft.fileTreeLoadingPaths = draft.fileTreeLoadingPaths.filter( p => p !== action.payload!.path && p !== '' ); }), - [String(openTreePath)]: (state, action: Action) => - produce(state, draft => { - let path = action.payload!; - const openedPaths = state.openedPaths; - const pathSegs = path.split('/'); - while (!openedPaths.includes(path)) { - draft.openedPaths.push(path); - pathSegs.pop(); - if (pathSegs.length <= 0) { - break; - } - path = pathSegs.join('/'); - } - }), - [String(closeTreePath)]: (state, action: Action) => - produce(state, draft => { - const path = action.payload!; - const isSubFolder = (p: string) => p.startsWith(path + '/'); - draft.openedPaths = state.openedPaths.filter(p => !(p === path || isSubFolder(p))); - }), [String(routePathChange)]: clearState, [String(repoChange)]: clearState, [String(revisionChange)]: clearState, diff --git a/x-pack/legacy/plugins/code/public/reducers/repository_management.ts b/x-pack/legacy/plugins/code/public/reducers/repository_management.ts index 83576468f616..d43cb1fe4785 100644 --- a/x-pack/legacy/plugins/code/public/reducers/repository_management.ts +++ b/x-pack/legacy/plugins/code/public/reducers/repository_management.ts @@ -6,6 +6,7 @@ import produce from 'immer'; import { Action, handleActions } from 'redux-actions'; +import { i18n } from '@kbn/i18n'; import { Repository, RepoConfigs, RepositoryConfig } from '../../model'; import { @@ -89,22 +90,35 @@ export const repositoryManagement = handleActions< draft.importLoading = true; }), [String(importRepoSuccess)]: (state, action: Action) => + // TODO is it possible and how to deal with action.payload === undefined? produce(state, draft => { draft.importLoading = false; draft.showToast = true; draft.toastType = ToastType.success; - draft.toastMessage = `${action.payload!.name} has been successfully submitted!`; + draft.toastMessage = i18n.translate( + 'xpack.code.repositoryManagement.repoSubmittedMessage', + { + defaultMessage: '{name} has been successfully submitted!', + values: { name: action.payload!.name }, + } + ); draft.repositories = [...state.repositories, action.payload!]; }), [String(importRepoFailed)]: (state, action: Action) => produce(state, draft => { if (action.payload) { if (action.payload.res.status === 304) { - draft.toastMessage = 'This Repository has already been imported!'; + draft.toastMessage = i18n.translate( + 'xpack.code.repositoryManagement.repoImportedMessage', + { + defaultMessage: 'This Repository has already been imported!', + } + ); draft.showToast = true; draft.toastType = ToastType.warning; draft.importLoading = false; } else { + // TODO add localication for those messages draft.toastMessage = action.payload.body.message; draft.showToast = true; draft.toastType = ToastType.danger; diff --git a/x-pack/legacy/plugins/code/public/reducers/symbol.ts b/x-pack/legacy/plugins/code/public/reducers/symbol.ts index 6c8f8daf8aed..33d92953c69a 100644 --- a/x-pack/legacy/plugins/code/public/reducers/symbol.ts +++ b/x-pack/legacy/plugins/code/public/reducers/symbol.ts @@ -8,7 +8,7 @@ import produce from 'immer'; import _ from 'lodash'; import { Action, handleActions } from 'redux-actions'; -import { SymbolInformation } from 'vscode-languageserver-types/lib/esm/main'; +import { DocumentSymbol } from 'vscode-languageserver-types'; import { closeSymbolPath, loadStructure, @@ -22,7 +22,7 @@ import { languageServerInitializing } from '../actions/language_server'; import { routePathChange, repoChange, revisionChange, filePathChange } from '../actions/route'; export interface SymbolState { - symbols: { [key: string]: SymbolInformation[] }; + symbols: { [key: string]: DocumentSymbol[] }; structureTree: { [key: string]: SymbolWithMembers[] }; error?: Error; loading: boolean; diff --git a/x-pack/legacy/plugins/code/public/sagas/blame.ts b/x-pack/legacy/plugins/code/public/sagas/blame.ts index d79221f9c63e..33b356a893d6 100644 --- a/x-pack/legacy/plugins/code/public/sagas/blame.ts +++ b/x-pack/legacy/plugins/code/public/sagas/blame.ts @@ -5,16 +5,16 @@ */ import { Action } from 'redux-actions'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { call, put, takeEvery } from 'redux-saga/effects'; import { Match } from '../actions'; import { loadBlame, loadBlameFailed, LoadBlamePayload, loadBlameSuccess } from '../actions/blame'; import { blamePattern } from './patterns'; function requestBlame(repoUri: string, revision: string, path: string) { - return kfetch({ - pathname: `/api/code/repo/${repoUri}/blame/${encodeURIComponent(revision)}/${path}`, - }); + return npStart.core.http.get( + `/api/code/repo/${repoUri}/blame/${encodeURIComponent(revision)}/${path}` + ); } function* handleFetchBlame(action: Action) { diff --git a/x-pack/legacy/plugins/code/public/sagas/commit.ts b/x-pack/legacy/plugins/code/public/sagas/commit.ts index 4235f047865f..93a5c74e19c4 100644 --- a/x-pack/legacy/plugins/code/public/sagas/commit.ts +++ b/x-pack/legacy/plugins/code/public/sagas/commit.ts @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import { Action } from 'redux-actions'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { call, put, takeEvery } from 'redux-saga/effects'; import { loadCommit, loadCommitFailed, loadCommitSuccess, Match } from '../actions'; import { commitRoutePattern } from './patterns'; function requestCommit(repo: string, commitId: string) { - return kfetch({ - pathname: `/api/code/repo/${repo}/diff/${commitId}`, - }); + return npStart.core.http.get(`/api/code/repo/${repo}/diff/${commitId}`); } function* handleLoadCommit(action: Action) { diff --git a/x-pack/legacy/plugins/code/public/sagas/editor.ts b/x-pack/legacy/plugins/code/public/sagas/editor.ts index b4a75e14e324..437846139d27 100644 --- a/x-pack/legacy/plugins/code/public/sagas/editor.ts +++ b/x-pack/legacy/plugins/code/public/sagas/editor.ts @@ -6,7 +6,7 @@ import queryString from 'querystring'; import { Action } from 'redux-actions'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { TextDocumentPositionParams } from 'vscode-languageserver'; import Url from 'url'; import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; @@ -16,8 +16,6 @@ import { closeReferences, fetchFile, FetchFileResponse, - fetchRepoBranches, - fetchRepoCommits, fetchRepoTree, fetchTreeCommits, findReferences, @@ -25,11 +23,9 @@ import { findReferencesSuccess, loadStructure, Match, - resetRepoTree, revealPosition, fetchRepos, turnOnDefaultRepoScope, - fetchRootRepoTree, } from '../actions'; import { loadRepo, loadRepoFailed, loadRepoSuccess } from '../actions/status'; import { PathTypes } from '../common/types'; @@ -43,7 +39,7 @@ import { repoScopeSelector, urlQueryStringSelector, createTreeSelector, - getTreeRevision, + reposSelector, } from '../selectors'; import { history } from '../utils/url'; import { mainRoutePattern } from './patterns'; @@ -60,9 +56,7 @@ function* handleReferences(action: Action) { } function requestFindReferences(params: TextDocumentPositionParams) { - return kfetch({ - pathname: `/api/code/lsp/findReferences`, - method: 'POST', + return npStart.core.http.post(`/api/code/lsp/findReferences`, { body: JSON.stringify(params), }); } @@ -134,7 +128,7 @@ function* handleFile(repoUri: string, file: string, revision: string) { } function fetchRepo(repoUri: string) { - return kfetch({ pathname: `/api/code/repo/${repoUri}` }); + return npStart.core.http.get(`/api/code/repo/${repoUri}`); } function* loadRepoSaga(action: any) { @@ -157,9 +151,11 @@ export function* watchLoadRepo() { } function* handleMainRouteChange(action: Action) { - // in source view page, we need repos as default repo scope options when no query input - yield put(fetchRepos()); - + const repos = yield select(reposSelector); + if (repos.length === 0) { + // in source view page, we need repos as default repo scope options when no query input + yield put(fetchRepos()); + } const { location } = action.payload!; const search = location.search.startsWith('?') ? location.search.substring(1) : location.search; const queryParams = queryString.parse(search); @@ -169,8 +165,6 @@ function* handleMainRouteChange(action: Action) { if (goto) { position = parseGoto(goto); } - yield put(loadRepo(repoUri)); - yield put(fetchRepoBranches({ uri: repoUri })); if (file) { if ([PathTypes.blob, PathTypes.blame].includes(pathType as PathTypes)) { yield put(revealPosition(position)); @@ -188,14 +182,6 @@ function* handleMainRouteChange(action: Action) { } } const lastRequestPath = yield select(lastRequestPathSelector); - const currentTree: FileTree = yield select(getTree); - const currentTreeRevision: string = yield select(getTreeRevision); - // repo changed - if (currentTree.repoUri !== repoUri || revision !== currentTreeRevision) { - yield put(resetRepoTree()); - yield put(fetchRepoCommits({ uri: repoUri, revision })); - yield put(fetchRootRepoTree({ uri: repoUri, revision })); - } const tree = yield select(getTree); const isDir = pathType === PathTypes.tree; function isTreeLoaded(isDirectory: boolean, targetTree: FileTree | null) { diff --git a/x-pack/legacy/plugins/code/public/sagas/file.ts b/x-pack/legacy/plugins/code/public/sagas/file.ts index d562bf9efec9..01c1785e9e6a 100644 --- a/x-pack/legacy/plugins/code/public/sagas/file.ts +++ b/x-pack/legacy/plugins/code/public/sagas/file.ts @@ -5,8 +5,7 @@ */ import { Action } from 'redux-actions'; -import chrome from 'ui/chrome'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import Url from 'url'; import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; @@ -93,10 +92,12 @@ function requestRepoTree({ if (parents) { query.parents = true; } - return kfetch({ - pathname: `/api/code/repo/${uri}/tree/${encodeURIComponent(revision)}/${path}`, - query, - }); + return npStart.core.http.get( + `/api/code/repo/${uri}/tree/${encodeURIComponent(revision)}/${path}`, + { + query, + } + ); } export function* watchFetchRepoTree() { @@ -127,9 +128,7 @@ function* handleFetchBranches(action: Action) { } function requestBranches({ uri }: FetchRepoPayload) { - return kfetch({ - pathname: `/api/code/repo/${uri}/references`, - }); + return npStart.core.http.get(`/api/code/repo/${uri}/references`); } function* handleFetchCommits(action: Action) { @@ -172,16 +171,19 @@ function requestCommits( count?: number ) { const pathStr = path ? `/${path}` : ''; - const options: any = { - pathname: `/api/code/repo/${uri}/history/${encodeURIComponent(revision)}${pathStr}`, - }; + let query: any = {}; if (loadMore) { - options.query = { after: 1 }; + query = { after: 1 }; } if (count) { - options.count = count; + query = { count }; } - return kfetch(options); + return npStart.core.http.get( + `/api/code/repo/${uri}/history/${encodeURIComponent(revision)}${pathStr}`, + { + query, + } + ); } export async function requestFile( @@ -194,7 +196,9 @@ export async function requestFile( if (line) { query.line = line; } - const response: Response = await fetch(chrome.addBasePath(Url.format({ pathname: url, query }))); + const response: Response = await fetch( + npStart.core.http.basePath.prepend(Url.format({ pathname: url, query })) + ); if (response.status >= 200 && response.status < 300) { const contentType = response.headers.get('Content-Type'); diff --git a/x-pack/legacy/plugins/code/public/sagas/index.ts b/x-pack/legacy/plugins/code/public/sagas/index.ts index 04534bcc2869..0b6e33de064f 100644 --- a/x-pack/legacy/plugins/code/public/sagas/index.ts +++ b/x-pack/legacy/plugins/code/public/sagas/index.ts @@ -49,9 +49,11 @@ import { import { watchRootRoute } from './setup'; import { watchRepoCloneSuccess, watchRepoDeleteFinished, watchStatusChange } from './status'; import { watchLoadStructure } from './structure'; -import { watchRoute } from './route'; +import { watchRoute, watchRepoChange, watchRepoOrRevisionChange } from './route'; export function* rootSaga() { + yield fork(watchRepoChange); + yield fork(watchRepoOrRevisionChange); yield fork(watchRoute); yield fork(watchRootRoute); yield fork(watchLoadCommit); diff --git a/x-pack/legacy/plugins/code/public/sagas/language_server.ts b/x-pack/legacy/plugins/code/public/sagas/language_server.ts index 927892e3f49b..c9e452201aa4 100644 --- a/x-pack/legacy/plugins/code/public/sagas/language_server.ts +++ b/x-pack/legacy/plugins/code/public/sagas/language_server.ts @@ -5,7 +5,7 @@ */ import { Action } from 'redux-actions'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { call, put, takeEvery } from 'redux-saga/effects'; import { loadLanguageServers, @@ -17,16 +17,11 @@ import { } from '../actions/language_server'; function fetchLangServers() { - return kfetch({ - pathname: '/api/code/install', - }); + return npStart.core.http.get('/api/code/install'); } function installLanguageServer(languageServer: string) { - return kfetch({ - pathname: `/api/code/install/${languageServer}`, - method: 'POST', - }); + return npStart.core.http.post(`/api/code/install/${languageServer}`); } function* handleInstallLanguageServer(action: Action) { diff --git a/x-pack/legacy/plugins/code/public/sagas/project_config.ts b/x-pack/legacy/plugins/code/public/sagas/project_config.ts index fb1672f2c65c..c417912f9da5 100644 --- a/x-pack/legacy/plugins/code/public/sagas/project_config.ts +++ b/x-pack/legacy/plugins/code/public/sagas/project_config.ts @@ -5,7 +5,7 @@ */ import { Action } from 'redux-actions'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { all, call, put, takeEvery } from 'redux-saga/effects'; import { Repository, RepositoryConfig } from '../../model'; import { @@ -18,9 +18,7 @@ import { import { loadConfigsFailed, loadConfigsSuccess } from '../actions/project_config'; function putProjectConfig(repoUri: string, config: RepositoryConfig) { - return kfetch({ - pathname: `/api/code/repo/config/${repoUri}`, - method: 'PUT', + return npStart.core.http.put(`/api/code/repo/config/${repoUri}`, { body: JSON.stringify(config), }); } @@ -40,9 +38,7 @@ export function* watchSwitchProjectLanguageServer() { } function fetchConfigs(repoUri: string) { - return kfetch({ - pathname: `/api/code/repo/config/${repoUri}`, - }); + return npStart.core.http.get(`/api/code/repo/config/${repoUri}`); } function* loadConfigs(action: Action) { diff --git a/x-pack/legacy/plugins/code/public/sagas/project_status.ts b/x-pack/legacy/plugins/code/public/sagas/project_status.ts index d4ee77761cd0..0a392b3959c3 100644 --- a/x-pack/legacy/plugins/code/public/sagas/project_status.ts +++ b/x-pack/legacy/plugins/code/public/sagas/project_status.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { Action } from 'redux-actions'; import { delay } from 'redux-saga'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { all, call, @@ -54,9 +54,7 @@ import { const REPO_STATUS_POLLING_FREQ_MS = 1000; function fetchStatus(repoUri: string) { - return kfetch({ - pathname: `/api/code/repo/status/${repoUri}`, - }); + return npStart.core.http.get(`/api/code/repo/status/${repoUri}`); } function* loadRepoListStatus(repos: Repository[]) { diff --git a/x-pack/legacy/plugins/code/public/sagas/repository.ts b/x-pack/legacy/plugins/code/public/sagas/repository.ts index 16d7b386648e..c8f5ce9a6755 100644 --- a/x-pack/legacy/plugins/code/public/sagas/repository.ts +++ b/x-pack/legacy/plugins/code/public/sagas/repository.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 { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { Action } from 'redux-actions'; import { call, put, takeEvery, takeLatest, take } from 'redux-saga/effects'; @@ -37,7 +37,7 @@ import { history } from '../utils/url'; import { adminRoutePattern } from './patterns'; function requestRepos(): any { - return kfetch({ pathname: '/api/code/repos' }); + return npStart.core.http.get('/api/code/repos'); } function* handleFetchRepos() { @@ -50,13 +50,11 @@ function* handleFetchRepos() { } function requestDeleteRepo(uri: string) { - return kfetch({ pathname: `/api/code/repo/${uri}`, method: 'delete' }); + return npStart.core.http.delete(`/api/code/repo/${uri}`); } function requestIndexRepo(uri: string) { - return kfetch({ - pathname: `/api/code/repo/index/${uri}`, - method: 'post', + return npStart.core.http.post(`/api/code/repo/index/${uri}`, { body: JSON.stringify({ reindex: true }), }); } @@ -92,9 +90,7 @@ function* handleIndexRepo(action: Action) { } function requestImportRepo(uri: string) { - return kfetch({ - pathname: '/api/code/repo', - method: 'post', + return npStart.core.http.post('/api/code/repo', { body: JSON.stringify({ url: uri }), }); } @@ -117,7 +113,7 @@ function* handleFetchRepoConfigs() { } function requestRepoConfigs() { - return kfetch({ pathname: '/api/code/workspace', method: 'get' }); + return npStart.core.http.get('/api/code/workspace'); } function* handleInitCmd(action: Action) { @@ -126,10 +122,8 @@ function* handleInitCmd(action: Action) { } function requestRepoInitCmd(repoUri: string) { - return kfetch({ - pathname: `/api/code/workspace/${repoUri}/master`, + return npStart.core.http.post(`/api/code/workspace/${repoUri}/master`, { query: { force: true }, - method: 'post', }); } function* handleGotoRepo(action: Action) { diff --git a/x-pack/legacy/plugins/code/public/sagas/route.ts b/x-pack/legacy/plugins/code/public/sagas/route.ts index c4a79103f38f..bafb0f5326f3 100644 --- a/x-pack/legacy/plugins/code/public/sagas/route.ts +++ b/x-pack/legacy/plugins/code/public/sagas/route.ts @@ -4,40 +4,77 @@ * you may not use this file except in compliance with the Elastic License. */ -import { put, takeEvery, select } from 'redux-saga/effects'; +import { put, takeEvery, select, takeLatest } from 'redux-saga/effects'; import { Action } from 'redux-actions'; -import { routeChange, Match } from '../actions'; -import { previousMatchSelector } from '../selectors'; +import { + routeChange, + Match, + loadRepo, + fetchRepoBranches, + fetchRepoCommits, + fetchRootRepoTree, +} from '../actions'; +import { previousMatchSelector, repoUriSelector, revisionSelector } from '../selectors'; import { routePathChange, repoChange, revisionChange, filePathChange } from '../actions/route'; import * as ROUTES from '../components/routes'; +const MAIN_ROUTES = [ROUTES.MAIN, ROUTES.MAIN_ROOT]; + +function* handleRepoOrRevisionChange() { + const repoUri = yield select(repoUriSelector); + const revision = yield select(revisionSelector); + yield put(fetchRepoCommits({ uri: repoUri, revision })); + yield put(fetchRootRepoTree({ uri: repoUri, revision })); +} + +export function* watchRepoOrRevisionChange() { + yield takeLatest([String(repoChange), String(revisionChange)], handleRepoOrRevisionChange); +} + const getRepoFromMatch = (match: Match) => - `${match.params.resources}/${match.params.org}/${match.params.repo}`; + `${match.params.resource}/${match.params.org}/${match.params.repo}`; function* handleRoute(action: Action) { const currentMatch = action.payload; const previousMatch = yield select(previousMatchSelector); - if (currentMatch.path !== previousMatch.path) { - yield put(routePathChange()); - } else if (currentMatch.path === ROUTES.MAIN) { - const currentRepo = getRepoFromMatch(currentMatch); - const previousRepo = getRepoFromMatch(previousMatch); - const currentRevision = currentMatch.params.revision; - const previousRevision = previousMatch.params.revision; - const currentFilePath = currentMatch.params.path; - const previousFilePath = previousMatch.params.path; - if (currentRepo !== previousRepo) { - yield put(repoChange()); - } - if (currentRevision !== previousRevision) { + if (MAIN_ROUTES.includes(currentMatch.path)) { + if (MAIN_ROUTES.includes(previousMatch.path)) { + const currentRepo = getRepoFromMatch(currentMatch); + const previousRepo = getRepoFromMatch(previousMatch); + const currentRevision = currentMatch.params.revision; + const previousRevision = previousMatch.params.revision; + const currentFilePath = currentMatch.params.path; + const previousFilePath = previousMatch.params.path; + if (currentRepo !== previousRepo) { + yield put(repoChange(currentRepo)); + } + if (currentRevision !== previousRevision) { + yield put(revisionChange()); + } + if (currentFilePath !== previousFilePath) { + yield put(filePathChange()); + } + } else { + yield put(routePathChange()); + const currentRepo = getRepoFromMatch(currentMatch); + yield put(repoChange(currentRepo)); yield put(revisionChange()); - } - if (currentFilePath !== previousFilePath) { yield put(filePathChange()); } + } else if (currentMatch.path !== previousMatch.path) { + yield put(routePathChange()); } } export function* watchRoute() { yield takeEvery(String(routeChange), handleRoute); } + +export function* handleRepoChange(action: Action) { + yield put(loadRepo(action.payload!)); + yield put(fetchRepoBranches({ uri: action.payload! })); +} + +export function* watchRepoChange() { + yield takeEvery(String(repoChange), handleRepoChange); +} diff --git a/x-pack/legacy/plugins/code/public/sagas/search.ts b/x-pack/legacy/plugins/code/public/sagas/search.ts index 44ede3442e3d..a9e8205d3b76 100644 --- a/x-pack/legacy/plugins/code/public/sagas/search.ts +++ b/x-pack/legacy/plugins/code/public/sagas/search.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import queryString from 'querystring'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { Action } from 'redux-actions'; import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'; @@ -52,9 +52,7 @@ function requestDocumentSearch(payload: DocumentSearchPayload) { } if (query && query.length > 0) { - return kfetch({ - pathname: `/api/code/search/doc`, - method: 'get', + return npStart.core.http.get(`/api/code/search/doc`, { query: queryParams, }); } else { @@ -76,9 +74,7 @@ function* handleDocumentSearch(action: Action) { } function requestRepositorySearch(q: string) { - return kfetch({ - pathname: `/api/code/search/repo`, - method: 'get', + return npStart.core.http.get(`/api/code/search/repo`, { query: { q }, }); } diff --git a/x-pack/legacy/plugins/code/public/sagas/setup.ts b/x-pack/legacy/plugins/code/public/sagas/setup.ts index 62f8cdb6cadb..7c7910b695d9 100644 --- a/x-pack/legacy/plugins/code/public/sagas/setup.ts +++ b/x-pack/legacy/plugins/code/public/sagas/setup.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { call, put, takeEvery } from 'redux-saga/effects'; import { checkSetupFailed, checkSetupSuccess } from '../actions'; import { rootRoutePattern, setupRoutePattern } from './patterns'; @@ -19,7 +19,7 @@ function* handleRootRoute() { } function requestSetup() { - return kfetch({ pathname: `/api/code/setup`, method: 'head' }); + return npStart.core.http.head(`/api/code/setup`); } export function* watchRootRoute() { diff --git a/x-pack/legacy/plugins/code/public/sagas/status.ts b/x-pack/legacy/plugins/code/public/sagas/status.ts index 84f7840a0ca0..796cbed837e6 100644 --- a/x-pack/legacy/plugins/code/public/sagas/status.ts +++ b/x-pack/legacy/plugins/code/public/sagas/status.ts @@ -6,7 +6,7 @@ import { Action } from 'redux-actions'; import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { isEqual } from 'lodash'; import { delay } from 'redux-saga'; @@ -138,10 +138,7 @@ function requestStatus(location: FetchFilePayload) { ? `/api/code/repo/${uri}/status/${revision}/${path}` : `/api/code/repo/${uri}/status/${revision}`; - return kfetch({ - pathname, - method: 'GET', - }); + return npStart.core.http.get(pathname); } export function* watchStatusChange() { diff --git a/x-pack/legacy/plugins/code/public/sagas/structure.ts b/x-pack/legacy/plugins/code/public/sagas/structure.ts index 6f2fbbc8e65c..010d6776530e 100644 --- a/x-pack/legacy/plugins/code/public/sagas/structure.ts +++ b/x-pack/legacy/plugins/code/public/sagas/structure.ts @@ -6,7 +6,7 @@ import { Action } from 'redux-actions'; import { call, put, select, takeEvery } from 'redux-saga/effects'; -import { SymbolInformation } from 'vscode-languageserver-types/lib/esm/main'; +import { DocumentSymbol } from 'vscode-languageserver-types'; import { LspRestClient, TextDocumentMethods } from '../../common/lsp_client'; import { loadStructure, @@ -15,84 +15,36 @@ import { StatusChanged, } from '../actions'; import { SymbolWithMembers } from '../actions/structure'; -import { matchContainerName } from '../utils/symbol_utils'; import { RepoFileStatus, StatusReport } from '../../common/repo_file_status'; import { RootState } from '../reducers'; import { toCanonicalUrl } from '../../common/uri_util'; -type Container = SymbolWithMembers | undefined; - -const SPECIAL_SYMBOL_NAME = '{...}'; -const SPECIAL_CONTAINER_NAME = ''; - const sortSymbol = (a: SymbolWithMembers, b: SymbolWithMembers) => { - const lineDiff = a.location.range.start.line - b.location.range.start.line; + const lineDiff = a.range.start.line - b.range.start.line; if (lineDiff === 0) { - return a.location.range.start.character - b.location.range.start.character; + return a.range.start.character - b.range.start.character; } else { return lineDiff; } }; -const generateStructureTree: (symbols: SymbolInformation[]) => SymbolWithMembers[] = symbols => { - const structureTree: SymbolWithMembers[] = []; - - function findContainer( - tree: SymbolWithMembers[], - containerName?: string - ): SymbolInformation | undefined { - if (containerName === undefined) { - return undefined; - } - const result = tree.find((s: SymbolInformation) => { - return matchContainerName(containerName, s.name); - }); - if (result) { - return result; - } else { - // TODO: Use Array.flat once supported - const subTree = tree.reduce( - (s, t) => (t.members ? s.concat(t.members) : s), - [] as SymbolWithMembers[] - ); - if (subTree.length > 0) { - return findContainer(subTree, containerName); - } else { - return undefined; - } - } +const generateStructureTree: (documentSymbol: DocumentSymbol, path: string) => SymbolWithMembers = ( + documentSymbol, + path +) => { + const currentPath = path ? `${path}/${documentSymbol.name}` : documentSymbol.name; + const structureTree: SymbolWithMembers = { + name: documentSymbol.name, + kind: documentSymbol.kind, + path: currentPath, + range: documentSymbol.range, + selectionRange: documentSymbol.selectionRange, + }; + if (documentSymbol.children) { + structureTree.members = documentSymbol.children + .sort(sortSymbol) + .map(ds => generateStructureTree(ds, currentPath)); } - - symbols - .sort(sortSymbol) - .forEach((s: SymbolInformation, index: number, arr: SymbolInformation[]) => { - let container: Container; - /** - * For Enum class in Java, the container name and symbol name that LSP gives are special. - * For more information, see https://github.com/elastic/codesearch/issues/580 - */ - if (s.containerName === SPECIAL_CONTAINER_NAME) { - container = _.findLast( - arr.slice(0, index), - (sy: SymbolInformation) => sy.name === SPECIAL_SYMBOL_NAME - ); - } else { - container = findContainer(structureTree, s.containerName); - } - if (container) { - if (!container.path) { - container.path = container.name; - } - if (container.members) { - container.members.push({ ...s, path: `${container.path}/${s.name}` }); - } else { - container.members = [{ ...s, path: `${container.path}/${s.name}` }]; - } - } else { - structureTree.push({ ...s, path: s.name }); - } - }); - return structureTree; }; @@ -135,8 +87,8 @@ export function* watchLoadStructure() { function* fetchSymbols(action: Action) { try { - const data = yield call(requestStructure, `git:/${action.payload}`); - const structureTree = generateStructureTree(data); + const data: DocumentSymbol[] = yield call(requestStructure, `git:/${action.payload}`); + const structureTree = data.sort(sortSymbol).map(ds => generateStructureTree(ds, '')); yield put(loadStructureSuccess({ path: action.payload!, data, structureTree })); } catch (e) { yield put(loadStructureFailed(e)); diff --git a/x-pack/legacy/plugins/code/public/selectors/index.ts b/x-pack/legacy/plugins/code/public/selectors/index.ts index 253229df8cbc..870cf1cee3e8 100644 --- a/x-pack/legacy/plugins/code/public/selectors/index.ts +++ b/x-pack/legacy/plugins/code/public/selectors/index.ts @@ -35,6 +35,7 @@ export const repoUriSelector = (state: RootState) => { const { resource, org, repo } = state.route.match.params; return `${resource}/${org}/${repo}`; }; +export const revisionSelector = (state: RootState) => state.route.match.params.revision; export const routeSelector = (state: RootState) => state.route.match; @@ -103,3 +104,4 @@ export const urlQueryStringSelector = (state: RootState) => state.route.match.lo export const previousMatchSelector = (state: RootState) => state.route.previousMatch; export const statusSelector = (state: RootState) => state.status.repoFileStatus; +export const reposSelector = (state: RootState) => state.repositoryManagement.repositories; diff --git a/x-pack/legacy/plugins/code/public/utils/symbol_utils.ts b/x-pack/legacy/plugins/code/public/utils/symbol_utils.ts deleted file mode 100644 index fe659c703b19..000000000000 --- a/x-pack/legacy/plugins/code/public/utils/symbol_utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const matchContainerName = (containerName: string, symbolName: string) => - new RegExp(`^${containerName}([<(].*[>)])?$`).test(symbolName); diff --git a/x-pack/legacy/plugins/code/server/__tests__/clone_worker.ts b/x-pack/legacy/plugins/code/server/__tests__/clone_worker.ts index db8d308a9c8d..988f4d75d53e 100644 --- a/x-pack/legacy/plugins/code/server/__tests__/clone_worker.ts +++ b/x-pack/legacy/plugins/code/server/__tests__/clone_worker.ts @@ -13,6 +13,7 @@ import rimraf from 'rimraf'; import sinon from 'sinon'; import { Repository } from '../../model'; +import { DiskWatermarkService } from '../disk_watermark'; import { GitOperations } from '../git_operations'; import { EsClient, Esqueue } from '../lib/esqueue'; import { Logger } from '../log'; @@ -106,6 +107,12 @@ describe('clone_worker_tests', () => { cancellationService.cancelCloneJob = cancelCloneJobSpy; cancellationService.registerCancelableCloneJob = registerCancelableCloneJobSpy; + // Setup DiskWatermarkService + const isLowWatermarkSpy = sinon.stub().resolves(false); + const diskWatermarkService: any = { + isLowWatermark: isLowWatermarkSpy, + }; + const cloneWorker = new CloneWorker( esQueue as Esqueue, log, @@ -114,7 +121,8 @@ describe('clone_worker_tests', () => { gitOps, {} as IndexWorker, (repoServiceFactory as any) as RepositoryServiceFactory, - cancellationService as CancellationSerivce + cancellationService as CancellationSerivce, + diskWatermarkService as DiskWatermarkService ); await cloneWorker.executeJob({ @@ -125,6 +133,7 @@ describe('clone_worker_tests', () => { timestamp: 0, }); + assert.ok(isLowWatermarkSpy.calledOnce); assert.ok(newInstanceSpy.calledOnce); assert.ok(cloneSpy.calledOnce); }); @@ -154,6 +163,12 @@ describe('clone_worker_tests', () => { cancellationService.cancelCloneJob = cancelCloneJobSpy; cancellationService.registerCancelableCloneJob = registerCancelableCloneJobSpy; + // Setup DiskWatermarkService + const isLowWatermarkSpy = sinon.stub().resolves(false); + const diskWatermarkService: any = { + isLowWatermark: isLowWatermarkSpy, + }; + const cloneWorker = new CloneWorker( esQueue as Esqueue, log, @@ -162,7 +177,8 @@ describe('clone_worker_tests', () => { gitOps, (indexWorker as any) as IndexWorker, {} as RepositoryServiceFactory, - cancellationService as CancellationSerivce + cancellationService as CancellationSerivce, + diskWatermarkService as DiskWatermarkService ); await cloneWorker.onJobCompleted( @@ -190,6 +206,8 @@ describe('clone_worker_tests', () => { // Index request is issued after a 1s delay. await delay(1000); assert.ok(enqueueJobSpy.calledOnce); + + assert.ok(isLowWatermarkSpy.notCalled); }); it('On clone job completed because of cancellation', async () => { @@ -217,6 +235,12 @@ describe('clone_worker_tests', () => { cancellationService.cancelCloneJob = cancelCloneJobSpy; cancellationService.registerCancelableCloneJob = registerCancelableCloneJobSpy; + // Setup DiskWatermarkService + const isLowWatermarkSpy = sinon.stub().resolves(false); + const diskWatermarkService: any = { + isLowWatermark: isLowWatermarkSpy, + }; + const cloneWorker = new CloneWorker( esQueue as Esqueue, log, @@ -225,7 +249,8 @@ describe('clone_worker_tests', () => { gitOps, (indexWorker as any) as IndexWorker, {} as RepositoryServiceFactory, - cancellationService as CancellationSerivce + cancellationService as CancellationSerivce, + diskWatermarkService as DiskWatermarkService ); await cloneWorker.onJobCompleted( @@ -252,6 +277,8 @@ describe('clone_worker_tests', () => { // Index request should not be issued after clone request is done. await delay(1000); assert.ok(enqueueJobSpy.notCalled); + + assert.ok(isLowWatermarkSpy.notCalled); }); it('On clone job enqueued.', async () => { @@ -272,6 +299,12 @@ describe('clone_worker_tests', () => { cancellationService.cancelCloneJob = cancelCloneJobSpy; cancellationService.registerCancelableCloneJob = registerCancelableCloneJobSpy; + // Setup DiskWatermarkService + const isLowWatermarkSpy = sinon.stub().resolves(false); + const diskWatermarkService: any = { + isLowWatermark: isLowWatermarkSpy, + }; + const cloneWorker = new CloneWorker( esQueue as Esqueue, log, @@ -280,7 +313,8 @@ describe('clone_worker_tests', () => { gitOps, {} as IndexWorker, {} as RepositoryServiceFactory, - cancellationService as CancellationSerivce + cancellationService as CancellationSerivce, + diskWatermarkService as DiskWatermarkService ); await cloneWorker.onJobEnqueued({ @@ -320,6 +354,12 @@ describe('clone_worker_tests', () => { cancellationService.cancelCloneJob = cancelCloneJobSpy; cancellationService.registerCancelableCloneJob = registerCancelableCloneJobSpy; + // Setup DiskWatermarkService + const isLowWatermarkSpy = sinon.stub().resolves(false); + const diskWatermarkService: any = { + isLowWatermark: isLowWatermarkSpy, + }; + const cloneWorker = new CloneWorker( esQueue as Esqueue, log, @@ -328,7 +368,8 @@ describe('clone_worker_tests', () => { gitOps, {} as IndexWorker, (repoServiceFactory as any) as RepositoryServiceFactory, - cancellationService as CancellationSerivce + cancellationService as CancellationSerivce, + diskWatermarkService as DiskWatermarkService ); const result1 = await cloneWorker.executeJob({ @@ -342,6 +383,7 @@ describe('clone_worker_tests', () => { assert.ok(result1.repo === null); assert.ok(newInstanceSpy.notCalled); assert.ok(cloneSpy.notCalled); + assert.ok(isLowWatermarkSpy.calledOnce); const result2 = await cloneWorker.executeJob({ payload: { @@ -354,5 +396,66 @@ describe('clone_worker_tests', () => { assert.ok(result2.repo === null); assert.ok(newInstanceSpy.notCalled); assert.ok(cloneSpy.notCalled); + assert.ok(isLowWatermarkSpy.calledTwice); + }); + + it('Execute clone job failed because of low disk watermark', async () => { + // Setup RepositoryService + const cloneSpy = sinon.spy(); + const repoService = { + clone: emptyAsyncFunc, + }; + repoService.clone = cloneSpy; + const repoServiceFactory = { + newInstance: (): void => { + return; + }, + }; + const newInstanceSpy = sinon.fake.returns(repoService); + repoServiceFactory.newInstance = newInstanceSpy; + + // Setup CancellationService + const cancelCloneJobSpy = sinon.spy(); + const registerCancelableCloneJobSpy = sinon.spy(); + const cancellationService: any = { + cancelCloneJob: emptyAsyncFunc, + registerCancelableCloneJob: emptyAsyncFunc, + }; + cancellationService.cancelCloneJob = cancelCloneJobSpy; + cancellationService.registerCancelableCloneJob = registerCancelableCloneJobSpy; + + // Setup DiskWatermarkService + const isLowWatermarkSpy = sinon.stub().resolves(true); + const diskWatermarkService: any = { + isLowWatermark: isLowWatermarkSpy, + }; + + const cloneWorker = new CloneWorker( + esQueue as Esqueue, + log, + {} as EsClient, + serverOptions, + gitOps, + {} as IndexWorker, + (repoServiceFactory as any) as RepositoryServiceFactory, + cancellationService as CancellationSerivce, + diskWatermarkService as DiskWatermarkService + ); + + try { + await cloneWorker.executeJob({ + payload: { + url: 'https://github.com/Microsoft/TypeScript-Node-Starter.git', + }, + options: {}, + timestamp: 0, + }); + // This step should not be touched. + assert.ok(false); + } catch (error) { + assert.ok(isLowWatermarkSpy.calledOnce); + assert.ok(newInstanceSpy.notCalled); + assert.ok(cloneSpy.notCalled); + } }); }); diff --git a/x-pack/legacy/plugins/code/server/__tests__/lsp_incremental_indexer.ts b/x-pack/legacy/plugins/code/server/__tests__/lsp_incremental_indexer.ts index fc8f6378289e..9c00695c7a9d 100644 --- a/x-pack/legacy/plugins/code/server/__tests__/lsp_incremental_indexer.ts +++ b/x-pack/legacy/plugins/code/server/__tests__/lsp_incremental_indexer.ts @@ -200,14 +200,14 @@ describe('lsp_incremental_indexer unit tests', () => { // There are 5 MODIFIED items and 1 ADDED item. Only 1 file is in supported // language. Each file with supported language has 1 file + 1 symbol + 1 reference. - // Total doc indexed should be 8 * 3 = 15, + // Total doc indexed should be 6 * 2 + 4 = 16, // which can be fitted into a single batch index. assert.strictEqual(bulkSpy.callCount, 2); let total = 0; for (let i = 0; i < bulkSpy.callCount; i++) { total += bulkSpy.getCall(i).args[0].body.length; } - assert.strictEqual(total, 8 * 2); + assert.strictEqual(total, 16); // @ts-ignore }).timeout(20000); diff --git a/x-pack/legacy/plugins/code/server/__tests__/lsp_service.ts b/x-pack/legacy/plugins/code/server/__tests__/lsp_service.ts index 181f80b81594..efc3d75398e8 100644 --- a/x-pack/legacy/plugins/code/server/__tests__/lsp_service.ts +++ b/x-pack/legacy/plugins/code/server/__tests__/lsp_service.ts @@ -211,7 +211,7 @@ describe('lsp_service tests', () => { assert.ok(workspaceFolderExists); const controller = lspservice.controller; // @ts-ignore - const languageServer = controller.languageServerMap.typescript; + const languageServer = controller.languageServerMap.typescript[0]; const realWorkspacePath = fs.realpathSync(workspacePath); // @ts-ignore @@ -268,7 +268,7 @@ describe('lsp_service tests', () => { await lspservice.shutdown(); } // @ts-ignore - }).timeout(10000); + }).timeout(20000); it('should update if a worktree is not the newest', async () => { const lspservice = mockLspService(); diff --git a/x-pack/legacy/plugins/code/server/__tests__/multi_node.ts b/x-pack/legacy/plugins/code/server/__tests__/multi_node.ts index 549a10a2d32f..9c97a268962e 100644 --- a/x-pack/legacy/plugins/code/server/__tests__/multi_node.ts +++ b/x-pack/legacy/plugins/code/server/__tests__/multi_node.ts @@ -88,7 +88,8 @@ describe('code in multiple nodes', () => { port: codePort, }, plugins: { paths: [pluginPaths] }, - xpack: xpackOption, + xpack: { ...xpackOption, code: { codeNodeUrl: `http://localhost:${codePort}` } }, + logging: { silent: false }, }, }, }); @@ -109,6 +110,7 @@ describe('code in multiple nodes', () => { ...xpackOption, code: { codeNodeUrl: `http://localhost:${codePort}` }, }, + logging: { silent: true }, }; nonCodeNode = createRootWithCorePlugins(setting); await nonCodeNode.setup(); @@ -132,13 +134,23 @@ describe('code in multiple nodes', () => { await esServer.stop(); }); + function delay(ms: number) { + return new Promise(resolve1 => { + setTimeout(resolve1, ms); + }); + } + it('Code node setup should be ok', async () => { + await delay(6000); await request.get(kbnRootServer, '/api/code/setup').expect(200); - }); + // @ts-ignore + }).timeout(20000); it('Non-code node setup should be ok', async () => { + await delay(1000); await request.get(nonCodeNode, '/api/code/setup').expect(200); - }); + // @ts-ignore + }).timeout(5000); it('Non-code node setup should fail if code node is shutdown', async () => { await kbn.stop(); diff --git a/x-pack/legacy/plugins/code/server/disk_watermark.ts b/x-pack/legacy/plugins/code/server/disk_watermark.ts new file mode 100644 index 000000000000..549d932cd9b3 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/disk_watermark.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 checkDiskSpace from 'check-disk-space'; + +export class DiskWatermarkService { + constructor(private readonly diskWatermarkLowMb: number, private readonly repoPath: string) {} + + public async isLowWatermark(): Promise { + try { + const { free } = await checkDiskSpace(this.repoPath); + const availableMb = free / 1024 / 1024; + return availableMb <= this.diskWatermarkLowMb; + } catch (err) { + return true; + } + } +} diff --git a/x-pack/legacy/plugins/code/server/distributed/apis/git_api.ts b/x-pack/legacy/plugins/code/server/distributed/apis/git_api.ts new file mode 100644 index 000000000000..35af1adaee17 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/apis/git_api.ts @@ -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 fileType from 'file-type'; +import Boom from 'boom'; +import { Commit, Oid, Revwalk } from '@elastic/nodegit'; +import { commitInfo, GitOperations } from '../../git_operations'; +import { FileTree } from '../../../model'; +import { RequestContext, ServiceHandlerFor } from '../service_definition'; +import { extractLines } from '../../utils/buffer'; +import { detectLanguage } from '../../utils/detect_language'; +import { TEXT_FILE_LIMIT } from '../../../common/file'; +import { CommitInfo, ReferenceInfo } from '../../../model/commit'; +import { CommitDiff } from '../../../common/git_diff'; +import { GitBlame } from '../../../common/git_blame'; + +interface FileLocation { + uri: string; + path: string; + revision: string; +} +export const GitServiceDefinitionOption = { routePrefix: '/api/code/internal/git' }; +export const GitServiceDefinition = { + fileTree: { + request: {} as { + uri: string; + path: string; + revision: string; + skip: number; + limit: number; + withParents: boolean; + flatten: boolean; + }, + response: {} as FileTree, + }, + blob: { + request: {} as FileLocation & { line?: string }, + response: {} as { + isBinary: boolean; + imageType?: string; + content?: string; + lang?: string; + }, + }, + raw: { + request: {} as FileLocation, + response: {} as { + isBinary: boolean; + content: string; + }, + }, + history: { + request: {} as FileLocation & { + count: number; + after: boolean; + }, + response: {} as CommitInfo[], + }, + branchesAndTags: { + request: {} as { uri: string }, + response: {} as ReferenceInfo[], + }, + commitDiff: { + request: {} as { uri: string; revision: string }, + response: {} as CommitDiff, + }, + blame: { + request: {} as FileLocation, + response: {} as GitBlame[], + }, + commit: { + request: {} as { uri: string; revision: string }, + response: {} as CommitInfo, + }, + headRevision: { + request: {} as { uri: string }, + response: {} as string, + }, +}; + +export const getGitServiceHandler = ( + gitOps: GitOperations +): ServiceHandlerFor => ({ + async fileTree( + { uri, path, revision, skip, limit, withParents, flatten }, + context: RequestContext + ) { + return await gitOps.fileTree(uri, path, revision, skip, limit, withParents, flatten); + }, + async blob({ uri, path, revision, line }) { + const blob = await gitOps.fileContent(uri, path, revision); + const isBinary = blob.isBinary(); + if (isBinary) { + const type = fileType(blob.content()); + if (type && type.mime && type.mime.startsWith('image/')) { + return { + isBinary, + imageType: type.mime, + content: blob.content().toString(), + }; + } else { + return { + isBinary, + }; + } + } else { + if (line) { + const [from, to] = line.split(','); + let fromLine = parseInt(from, 10); + let toLine = to === undefined ? fromLine + 1 : parseInt(to, 10); + if (fromLine > toLine) { + [fromLine, toLine] = [toLine, fromLine]; + } + const lines = extractLines(blob.content(), fromLine, toLine); + const lang = await detectLanguage(path, lines); + return { + isBinary, + lang, + content: lines, + }; + } else if (blob.content()!.length <= TEXT_FILE_LIMIT) { + const lang = await detectLanguage(path, blob.content()); + return { + isBinary, + lang, + content: blob.content().toString(), + }; + } else { + return { + isBinary, + }; + } + } + }, + async raw({ uri, path, revision }) { + const blob = await gitOps.fileContent(uri, path, revision); + const isBinary = blob.isBinary(); + return { + isBinary, + content: blob.content().toString(), + }; + }, + async history({ uri, path, revision, count, after }) { + const repository = await gitOps.openRepo(uri); + const commit = await gitOps.getCommitInfo(uri, revision); + if (commit === null) { + throw Boom.notFound(`commit ${revision} not found in repo ${uri}`); + } + const walk = repository.createRevWalk(); + walk.sorting(Revwalk.SORT.TIME); + const commitId = Oid.fromString(commit!.id); + walk.push(commitId); + let commits: Commit[]; + if (path) { + // magic number 10000: how many commits at the most to iterate in order to find the commits contains the path + const results = await walk.fileHistoryWalk(path, count, 10000); + commits = results.map(result => result.commit); + } else { + commits = await walk.getCommits(count); + } + if (after && commits.length > 0) { + if (commits[0].id().equal(commitId)) { + commits = commits.slice(1); + } + } + return commits.map(commitInfo); + }, + async branchesAndTags({ uri }) { + return await gitOps.getBranchAndTags(uri); + }, + async commitDiff({ uri, revision }) { + return await gitOps.getCommitDiff(uri, revision); + }, + async blame({ uri, path, revision }) { + return await gitOps.blame(uri, revision, path); + }, + async commit({ uri, revision }) { + return await gitOps.getCommitOr404(uri, revision); + }, + async headRevision({ uri }) { + return await gitOps.getHeadRevision(uri); + }, +}); diff --git a/x-pack/legacy/plugins/code/server/distributed/apis/index.ts b/x-pack/legacy/plugins/code/server/distributed/apis/index.ts new file mode 100644 index 000000000000..1a33cc7f220d --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/apis/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 * from './git_api'; +export * from './lsp_api'; +export * from './workspace_api'; +export * from './setup_api'; +export * from './repository_api'; diff --git a/x-pack/legacy/plugins/code/server/distributed/apis/lsp_api.ts b/x-pack/legacy/plugins/code/server/distributed/apis/lsp_api.ts new file mode 100644 index 000000000000..e1dd6d26607e --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/apis/lsp_api.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 { ResponseMessage } from 'vscode-jsonrpc/lib/messages'; +import { LspService } from '../../lsp/lsp_service'; +import { ServiceHandlerFor } from '../service_definition'; +import { LanguageServerDefinition } from '../../lsp/language_servers'; +import { LanguageServerStatus } from '../../../common/language_server'; +import { WorkspaceStatus } from '../../lsp/request_expander'; + +export const LspServiceDefinitionOption = { routePrefix: '/api/code/internal/lsp' }; +export const LspServiceDefinition = { + sendRequest: { + request: {} as { method: string; params: any; timeoutForInitializeMs?: number }, + response: {} as ResponseMessage, + }, + languageSeverDef: { + request: {} as { lang: string }, + response: {} as LanguageServerDefinition[], + }, + languageServerStatus: { + request: {} as { langName: string }, + response: {} as LanguageServerStatus, + }, + initializeState: { + request: {} as { repoUri: string; revision: string }, + response: {} as { [p: string]: WorkspaceStatus }, + }, +}; + +export const getLspServiceHandler = ( + lspService: LspService +): ServiceHandlerFor => ({ + async sendRequest({ method, params, timeoutForInitializeMs }) { + return await lspService.sendRequest(method, params, timeoutForInitializeMs); + }, + async languageSeverDef({ lang }) { + return lspService.getLanguageSeverDef(lang); + }, + async languageServerStatus({ langName }) { + return lspService.languageServerStatus(langName); + }, + async initializeState({ repoUri, revision }) { + return await lspService.initializeState(repoUri, revision); + }, +}); diff --git a/x-pack/legacy/plugins/code/server/distributed/apis/repository_api.ts b/x-pack/legacy/plugins/code/server/distributed/apis/repository_api.ts new file mode 100644 index 000000000000..384ebc56acca --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/apis/repository_api.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ServiceHandlerFor } from '../service_definition'; +import { CloneWorker, DeleteWorker, IndexWorker } from '../../queue'; + +export const RepositoryServiceDefinition = { + clone: { + request: {} as { url: string }, + response: {}, + }, + delete: { + request: {} as { uri: string }, + response: {}, + }, + index: { + request: {} as { + uri: string; + revision: string | undefined; + enforceReindex: boolean; + }, + response: {}, + }, +}; + +export const getRepositoryHandler = ( + cloneWorker: CloneWorker, + deleteWorker: DeleteWorker, + indexWorker: IndexWorker +): ServiceHandlerFor => ({ + async clone(payload: { url: string }) { + await cloneWorker.enqueueJob(payload, {}); + return {}; + }, + async delete(payload: { uri: string }) { + await deleteWorker.enqueueJob(payload, {}); + return {}; + }, + async index(payload: { uri: string; revision: string | undefined; enforceReindex: boolean }) { + await indexWorker.enqueueJob(payload, {}); + return {}; + }, +}); diff --git a/x-pack/legacy/plugins/code/server/distributed/apis/setup_api.ts b/x-pack/legacy/plugins/code/server/distributed/apis/setup_api.ts new file mode 100644 index 000000000000..229470166568 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/apis/setup_api.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 { ServiceHandlerFor } from '../service_definition'; + +export const SetupDefinition = { + setup: { + request: {}, + response: {} as string, + }, +}; + +export const setupServiceHandler: ServiceHandlerFor = { + async setup() { + return 'ok'; + }, +}; diff --git a/x-pack/legacy/plugins/code/server/distributed/apis/workspace_api.ts b/x-pack/legacy/plugins/code/server/distributed/apis/workspace_api.ts new file mode 100644 index 000000000000..19fa9fc937d9 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/apis/workspace_api.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import { ServiceHandlerFor } from '../service_definition'; +import { WorkspaceHandler } from '../../lsp/workspace_handler'; +import { RepoConfig } from '../../../model'; +import { WorkspaceCommand } from '../../lsp/workspace_command'; +import { Logger } from '../../log'; + +export const WorkspaceDefinition = { + initCmd: { + request: {} as { repoUri: string; revision: string; repoConfig: RepoConfig; force: boolean }, + response: {}, + }, +}; + +export const getWorkspaceHandler = ( + server: Server, + workspaceHandler: WorkspaceHandler +): ServiceHandlerFor => ({ + async initCmd({ repoUri, revision, repoConfig, force }) { + try { + const { workspaceDir, workspaceRevision } = await workspaceHandler.openWorkspace( + repoUri, + revision + ); + const log = new Logger(server, ['workspace', repoUri]); + + const workspaceCmd = new WorkspaceCommand(repoConfig, workspaceDir, workspaceRevision, log); + await workspaceCmd.runInit(force); + return {}; + } catch (e) { + if (e.isBoom) { + return e; + } + } + }, +}); diff --git a/x-pack/legacy/plugins/code/server/distributed/code_services.test.ts b/x-pack/legacy/plugins/code/server/distributed/code_services.test.ts new file mode 100644 index 000000000000..eaefdaf5448e --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/code_services.test.ts @@ -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 { Request, Server } from 'hapi'; +import { createTestHapiServer } from '../test_utils'; +import { LocalHandlerAdapter } from './local_handler_adapter'; +import { CodeServerRouter } from '../security'; +import { RequestContext, ServiceHandlerFor } from './service_definition'; +import { CodeNodeAdapter, RequestPayload } from './multinode/code_node_adapter'; +import { DEFAULT_SERVICE_OPTION } from './service_handler_adapter'; +import { NonCodeNodeAdapter } from './multinode/non_code_node_adapter'; +import { CodeServices } from './code_services'; +import { Logger } from '../log'; + +let hapiServer: Server = createTestHapiServer(); +const log = new Logger(hapiServer); + +let server: CodeServerRouter = new CodeServerRouter(hapiServer); +beforeEach(async () => { + hapiServer = createTestHapiServer(); + server = new CodeServerRouter(hapiServer); +}); +const TestDefinition = { + test1: { + request: {} as { name: string }, + response: {} as { result: string }, + }, + test2: { + request: {}, + response: {} as RequestContext, + routePath: 'userDefinedPath', + }, +}; + +export const testServiceHandler: ServiceHandlerFor = { + async test1({ name }) { + return { result: `hello ${name}` }; + }, + async test2(_, context: RequestContext) { + return context; + }, +}; + +test('local adapter should work', async () => { + const services = new CodeServices(new LocalHandlerAdapter()); + services.registerHandler(TestDefinition, testServiceHandler); + const testApi = services.serviceFor(TestDefinition); + const endpoint = await services.locate({} as Request, ''); + const { result } = await testApi.test1(endpoint, { name: 'tester' }); + expect(result).toBe(`hello tester`); +}); + +test('multi-node adapter should register routes', async () => { + const services = new CodeServices(new CodeNodeAdapter(server, log)); + services.registerHandler(TestDefinition, testServiceHandler); + const prefix = DEFAULT_SERVICE_OPTION.routePrefix; + + const path1 = `${prefix}/test1`; + const response = await hapiServer.inject({ + method: 'POST', + url: path1, + payload: { params: { name: 'tester' } }, + }); + expect(response.statusCode).toBe(200); + const { data } = JSON.parse(response.payload); + expect(data.result).toBe(`hello tester`); +}); + +test('non-code-node could send request to code-node', async () => { + const codeNode = new CodeServices(new CodeNodeAdapter(server, log)); + const codeNodeUrl = 'http://localhost:5601'; + const nonCodeNodeAdapter = new NonCodeNodeAdapter(codeNodeUrl, log); + const nonCodeNode = new CodeServices(nonCodeNodeAdapter); + // replace client request fn to hapi.inject + nonCodeNodeAdapter.requestFn = async ( + baseUrl: string, + path: string, + payload: RequestPayload, + originRequest: Request + ) => { + expect(baseUrl).toBe(codeNodeUrl); + const response = await hapiServer.inject({ + method: 'POST', + url: path, + headers: originRequest.headers, + payload, + }); + expect(response.statusCode).toBe(200); + return JSON.parse(response.payload); + }; + codeNode.registerHandler(TestDefinition, testServiceHandler); + nonCodeNode.registerHandler(TestDefinition, null); + const testApi = nonCodeNode.serviceFor(TestDefinition); + const fakeRequest = ({ + path: 'fakePath', + headers: { + fakeHeader: 'fakeHeaderValue', + }, + } as unknown) as Request; + const fakeResource = 'fakeResource'; + const endpoint = await nonCodeNode.locate(fakeRequest, fakeResource); + const { result } = await testApi.test1(endpoint, { name: 'tester' }); + expect(result).toBe(`hello tester`); + + const context = await testApi.test2(endpoint, {}); + expect(context.resource).toBe(fakeResource); + expect(context.path).toBe(fakeRequest.path); +}); diff --git a/x-pack/legacy/plugins/code/server/distributed/code_services.ts b/x-pack/legacy/plugins/code/server/distributed/code_services.ts new file mode 100644 index 000000000000..0ff6e239e72a --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/code_services.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 { ServiceDefinition, ServiceHandlerFor, ServiceMethodMap } from './service_definition'; +import { + DEFAULT_SERVICE_OPTION, + ServiceHandlerAdapter, + ServiceRegisterOptions, +} from './service_handler_adapter'; +import { Endpoint } from './resource_locator'; +import { RequestFacade } from '../../'; + +export class CodeServices { + constructor(private readonly adapter: ServiceHandlerAdapter) {} + + public registerHandler( + serviceDefinition: serviceDefinition, + serviceHandler: ServiceHandlerFor | null, + options: ServiceRegisterOptions = DEFAULT_SERVICE_OPTION + ) { + this.adapter.registerHandler(serviceDefinition, serviceHandler, options); + } + + public locate(req: RequestFacade, resource: string): Promise { + return this.adapter.locator.locate(req, resource); + } + + public isResourceLocal(resource: string): Promise { + return this.adapter.locator.isResourceLocal(resource); + } + + public serviceFor(serviceDefinition: def): ServiceMethodMap { + return this.adapter.getService(serviceDefinition); + } +} diff --git a/x-pack/legacy/plugins/code/server/distributed/local_endpoint.ts b/x-pack/legacy/plugins/code/server/distributed/local_endpoint.ts new file mode 100644 index 000000000000..689ecc7fc641 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/local_endpoint.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 { Request } from 'hapi'; +import { Endpoint } from './resource_locator'; +import { RequestContext } from './service_definition'; + +export class LocalEndpoint implements Endpoint { + constructor(readonly httpRequest: Request, readonly resource: string) {} + + toContext(): RequestContext { + return { + resource: this.resource, + path: this.httpRequest.path, + } as RequestContext; + } +} diff --git a/x-pack/legacy/plugins/code/server/distributed/local_handler_adapter.ts b/x-pack/legacy/plugins/code/server/distributed/local_handler_adapter.ts new file mode 100644 index 000000000000..a1d19f2cf66a --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/local_handler_adapter.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 { Request } from 'hapi'; +import { ServiceHandlerAdapter } from './service_handler_adapter'; +import { ServiceDefinition, ServiceHandlerFor, ServiceMethodMap } from './service_definition'; +import { Endpoint, ResourceLocator } from './resource_locator'; +import { LocalEndpoint } from './local_endpoint'; + +export class LocalHandlerAdapter implements ServiceHandlerAdapter { + handlers: Map = new Map(); + + registerHandler( + serviceDefinition: def, + serviceHandler: ServiceHandlerFor | null + ) { + if (!serviceHandler) { + throw new Error("Local service handler can't be null!"); + } + const dispatchedHandler: { [key: string]: any } = {}; + // eslint-disable-next-line guard-for-in + for (const method in serviceDefinition) { + dispatchedHandler[method] = function(endpoint: Endpoint, params: any) { + return serviceHandler[method](params, endpoint.toContext()); + }; + } + this.handlers.set(serviceDefinition, dispatchedHandler); + return dispatchedHandler as ServiceMethodMap; + } + + getService(serviceDefinition: def): ServiceMethodMap { + const serviceHandler = this.handlers.get(serviceDefinition); + if (serviceHandler) { + return serviceHandler as ServiceMethodMap; + } else { + throw new Error(`handler for ${serviceDefinition} not found`); + } + } + + locator: ResourceLocator = { + async locate(httpRequest: Request, resource: string): Promise { + return Promise.resolve(new LocalEndpoint(httpRequest, resource)); + }, + + isResourceLocal(resource: string): Promise { + return Promise.resolve(true); + }, + }; +} diff --git a/x-pack/legacy/plugins/code/server/distributed/multinode/code_node_adapter.ts b/x-pack/legacy/plugins/code/server/distributed/multinode/code_node_adapter.ts new file mode 100644 index 000000000000..6b40a7a063e6 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/multinode/code_node_adapter.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 { Request } from 'hapi'; +import util from 'util'; +import Boom from 'boom'; +import { + DEFAULT_SERVICE_OPTION, + ServiceHandlerAdapter, + ServiceRegisterOptions, +} from '../service_handler_adapter'; +import { Endpoint, ResourceLocator } from '../resource_locator'; +import { + RequestContext, + ServiceDefinition, + ServiceHandlerFor, + ServiceMethodMap, +} from '../service_definition'; +import { CodeServerRouter } from '../../security'; +import { LocalHandlerAdapter } from '../local_handler_adapter'; +import { LocalEndpoint } from '../local_endpoint'; +import { Logger } from '../../log'; + +export interface RequestPayload { + context: RequestContext; + params: any; +} + +export class CodeNodeAdapter implements ServiceHandlerAdapter { + localAdapter: LocalHandlerAdapter = new LocalHandlerAdapter(); + constructor(private readonly server: CodeServerRouter, private readonly log: Logger) {} + + locator: ResourceLocator = { + async locate(httpRequest: Request, resource: string): Promise { + return Promise.resolve(new LocalEndpoint(httpRequest, resource)); + }, + + isResourceLocal(resource: string): Promise { + return Promise.resolve(false); + }, + }; + + getService(serviceDefinition: def): ServiceMethodMap { + // services on code node dispatch to local directly + return this.localAdapter.getService(serviceDefinition); + } + + registerHandler( + serviceDefinition: def, + serviceHandler: ServiceHandlerFor | null, + options: ServiceRegisterOptions = DEFAULT_SERVICE_OPTION + ) { + if (!serviceHandler) { + throw new Error("Code node service handler can't be null!"); + } + const serviceMethodMap = this.localAdapter.registerHandler(serviceDefinition, serviceHandler); + // eslint-disable-next-line guard-for-in + for (const method in serviceDefinition) { + const d = serviceDefinition[method]; + const path = `${options.routePrefix}/${d.routePath || method}`; + // register routes, receive requests from non-code node. + this.server.route({ + method: 'post', + path, + handler: async (req: Request) => { + const { context, params } = req.payload as RequestPayload; + this.log.debug(`Receiving RPC call ${req.url.path} ${util.inspect(params)}`); + const endpoint: Endpoint = { + toContext(): RequestContext { + return context; + }, + }; + try { + const data = await serviceMethodMap[method](endpoint, params); + return { data }; + } catch (e) { + if (!Boom.isBoom(e)) { + throw Boom.boomify(e, { statusCode: 500 }); + } else { + throw e; + } + } + }, + }); + } + return serviceMethodMap; + } +} diff --git a/x-pack/legacy/plugins/code/server/distributed/multinode/code_node_endpoint.ts b/x-pack/legacy/plugins/code/server/distributed/multinode/code_node_endpoint.ts new file mode 100644 index 000000000000..048b7c81dfe6 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/multinode/code_node_endpoint.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Request } from 'hapi'; +import { LocalEndpoint } from '../local_endpoint'; + +export class CodeNodeEndpoint extends LocalEndpoint { + constructor( + public readonly httpRequest: Request, + public readonly resource: string, + public readonly codeNodeUrl: string + ) { + super(httpRequest, resource); + } +} diff --git a/x-pack/legacy/plugins/code/server/distributed/multinode/code_node_resource_locator.ts b/x-pack/legacy/plugins/code/server/distributed/multinode/code_node_resource_locator.ts new file mode 100644 index 000000000000..fa3d3958963f --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/multinode/code_node_resource_locator.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 { Request } from 'hapi'; +import { Endpoint, ResourceLocator } from '../resource_locator'; +import { CodeNodeEndpoint } from './code_node_endpoint'; + +export class CodeNodeResourceLocator implements ResourceLocator { + constructor(private readonly codeNodeUrl: string) {} + + async locate(httpRequest: Request, resource: string): Promise { + return Promise.resolve(new CodeNodeEndpoint(httpRequest, resource, this.codeNodeUrl)); + } + + isResourceLocal(resource: string): Promise { + return Promise.resolve(false); + } +} diff --git a/x-pack/legacy/plugins/code/server/distributed/multinode/non_code_node_adapter.ts b/x-pack/legacy/plugins/code/server/distributed/multinode/non_code_node_adapter.ts new file mode 100644 index 000000000000..7e0ba0d7681c --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/multinode/non_code_node_adapter.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Wreck from '@hapi/wreck'; +import util from 'util'; +import Boom from 'boom'; +import { Request } from 'hapi'; +import * as http from 'http'; +import { + DEFAULT_SERVICE_OPTION, + ServiceHandlerAdapter, + ServiceRegisterOptions, +} from '../service_handler_adapter'; +import { ResourceLocator } from '../resource_locator'; +import { ServiceDefinition, ServiceHandlerFor, ServiceMethodMap } from '../service_definition'; +import { CodeNodeResourceLocator } from './code_node_resource_locator'; +import { CodeNodeEndpoint } from './code_node_endpoint'; +import { RequestPayload } from './code_node_adapter'; +import { Logger } from '../../log'; + +const pickHeaders = ['authorization']; + +function filterHeaders(originRequest: Request) { + const result: { [name: string]: string } = {}; + for (const header of pickHeaders) { + if (originRequest.headers[header]) { + result[header] = originRequest.headers[header]; + } + } + return result; +} + +export class NonCodeNodeAdapter implements ServiceHandlerAdapter { + handlers: Map = new Map(); + + constructor(private readonly codeNodeUrl: string, private readonly log: Logger) {} + + locator: ResourceLocator = new CodeNodeResourceLocator(this.codeNodeUrl); + + getService(serviceDefinition: def): ServiceMethodMap { + const serviceHandler = this.handlers.get(serviceDefinition); + if (!serviceHandler) { + // we don't need implement code for service, so we can register here. + this.registerHandler(serviceDefinition, null); + } + return serviceHandler as ServiceMethodMap; + } + + registerHandler( + serviceDefinition: def, + // serviceHandler will always be null here since it will be overridden by the request forwarding. + serviceHandler: ServiceHandlerFor | null, + options: ServiceRegisterOptions = DEFAULT_SERVICE_OPTION + ) { + const dispatchedHandler: { [key: string]: any } = {}; + // eslint-disable-next-line guard-for-in + for (const method in serviceDefinition) { + const d = serviceDefinition[method]; + const path = `${options.routePrefix}/${d.routePath || method}`; + dispatchedHandler[method] = async (endpoint: CodeNodeEndpoint, params: any) => { + const payload = { + context: endpoint.toContext(), + params, + }; + const { data } = await this.requestFn( + endpoint.codeNodeUrl, + path, + payload, + endpoint.httpRequest + ); + return data; + }; + } + this.handlers.set(serviceDefinition, dispatchedHandler); + return dispatchedHandler as ServiceMethodMap; + } + + async requestFn(baseUrl: string, path: string, payload: RequestPayload, originRequest: Request) { + const opt = { + baseUrl, + payload: JSON.stringify(payload), + // redirect all headers to CodeNode + headers: { ...filterHeaders(originRequest), 'kbn-xsrf': 'kibana' }, + }; + const promise = Wreck.request('POST', path, opt); + const res: http.IncomingMessage = await promise; + this.log.debug(`sending RPC call to ${baseUrl}${path} ${res.statusCode}`); + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + const buffer: Buffer = await Wreck.read(res, {}); + try { + return JSON.parse(buffer.toString(), (key, value) => { + return value && value.type === 'Buffer' ? Buffer.from(value.data) : value; + }); + } catch (e) { + this.log.error('parse json failed: ' + buffer.toString()); + throw Boom.boomify(e, { statusCode: 500 }); + } + } else { + this.log.error( + `received ${res.statusCode} from ${baseUrl}/${path}, params was ${util.inspect( + payload.params + )}` + ); + const body: Boom.Payload = await Wreck.read(res, { json: true }); + throw new Boom(body.message, { statusCode: res.statusCode || 500, data: body.error }); + } + } +} diff --git a/x-pack/legacy/plugins/code/server/distributed/resource_locator.ts b/x-pack/legacy/plugins/code/server/distributed/resource_locator.ts new file mode 100644 index 000000000000..bb4952eb8a1d --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/resource_locator.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 { Request } from 'hapi'; +import { RequestContext } from './service_definition'; + +export interface Endpoint { + toContext(): RequestContext; +} + +export interface ResourceLocator { + locate(req: Request, resource: string): Promise; + + /** + * Returns whether the resource resides on the local node. This should support both url and uri of the repository. + * + * @param resource the name of the resource. + */ + isResourceLocal(resource: string): Promise; +} diff --git a/x-pack/legacy/plugins/code/server/distributed/service_definition.ts b/x-pack/legacy/plugins/code/server/distributed/service_definition.ts new file mode 100644 index 000000000000..a3aa6643c76d --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/service_definition.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 { Endpoint } from './resource_locator'; + +export interface ServiceDefinition { + [method: string]: { request: any; response: any; routePath?: string }; +} + +export type MethodsFor = Extract< + keyof serviceDefinition, + string +>; + +export type MethodHandler< + serviceDefinition extends ServiceDefinition, + method extends MethodsFor +> = ( + request: RequestFor, + context: RequestContext +) => Promise>; + +export type ServiceHandlerFor = { + [method in MethodsFor]: MethodHandler; +}; + +export type RequestFor< + serviceDefinition extends ServiceDefinition, + method extends MethodsFor +> = serviceDefinition[method]['request']; + +export type ResponseFor< + serviceDefinition extends ServiceDefinition, + method extends MethodsFor +> = serviceDefinition[method]['response']; + +export interface RequestContext { + path: string; + resource: string; +} + +export type ServiceMethodMap = { + [method in MethodsFor]: ServiceMethod; +}; + +export type ServiceMethod< + serviceDefinition extends ServiceDefinition, + method extends MethodsFor +> = ( + endpoint: Endpoint, + request: RequestFor +) => Promise>; diff --git a/x-pack/legacy/plugins/code/server/distributed/service_handler_adapter.ts b/x-pack/legacy/plugins/code/server/distributed/service_handler_adapter.ts new file mode 100644 index 000000000000..88a81d278e74 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/service_handler_adapter.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 { ServiceDefinition, ServiceHandlerFor, ServiceMethodMap } from './service_definition'; +import { ResourceLocator } from './resource_locator'; + +export interface ServiceRegisterOptions { + routePrefix?: string; +} + +export const DEFAULT_SERVICE_OPTION: ServiceRegisterOptions = { + routePrefix: '/api/code/internal', +}; + +export interface ServiceHandlerAdapter { + locator: ResourceLocator; + getService(serviceDefinition: DEF): ServiceMethodMap; + registerHandler( + serviceDefinition: DEF, + serviceHandler: ServiceHandlerFor | null, + options: ServiceRegisterOptions + ): ServiceMethodMap; +} diff --git a/x-pack/legacy/plugins/code/server/index.ts b/x-pack/legacy/plugins/code/server/index.ts new file mode 100644 index 000000000000..510abaef68a0 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/index.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 { PluginInitializerContext } from 'src/core/server'; + +import * as constants from '../common/constants'; +import { CodePlugin } from './plugin'; + +export const codePlugin = (initializerContext: PluginInitializerContext) => + new CodePlugin(initializerContext); +export { constants }; diff --git a/x-pack/legacy/plugins/code/server/init.ts b/x-pack/legacy/plugins/code/server/init.ts deleted file mode 100644 index 2d1b44c0ba28..000000000000 --- a/x-pack/legacy/plugins/code/server/init.ts +++ /dev/null @@ -1,270 +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 crypto from 'crypto'; -import { Server } from 'hapi'; -import * as _ from 'lodash'; -import { i18n } from '@kbn/i18n'; - -import { XPackMainPlugin } from '../../xpack_main/xpack_main'; -import { GitOperations } from './git_operations'; -import { LspIndexerFactory, RepositoryIndexInitializerFactory, tryMigrateIndices } from './indexer'; -import { EsClient, Esqueue } from './lib/esqueue'; -import { Logger } from './log'; -import { InstallManager } from './lsp/install_manager'; -import { JAVA } from './lsp/language_servers'; -import { LspService } from './lsp/lsp_service'; -import { CancellationSerivce, CloneWorker, DeleteWorker, IndexWorker, UpdateWorker } from './queue'; -import { RepositoryConfigController } from './repository_config_controller'; -import { RepositoryServiceFactory } from './repository_service_factory'; -import { fileRoute } from './routes/file'; -import { installRoute } from './routes/install'; -import { lspRoute, symbolByQnameRoute } from './routes/lsp'; -import { redirectRoute } from './routes/redirect'; -import { repositoryRoute } from './routes/repository'; -import { documentSearchRoute, repositorySearchRoute, symbolSearchRoute } from './routes/search'; -import { setupRoute } from './routes/setup'; -import { workspaceRoute } from './routes/workspace'; -import { CloneScheduler, IndexScheduler, UpdateScheduler } from './scheduler'; -import { CodeServerRouter } from './security'; -import { ServerOptions } from './server_options'; -import { ServerLoggerFactory } from './utils/server_logger_factory'; -import { EsClientWithInternalRequest } from './utils/esclient_with_internal_request'; -import { checkCodeNode, checkRoute } from './routes/check'; -import { statusRoute } from './routes/status'; - -async function retryUntilAvailable( - func: () => Promise, - intervalMs: number, - retries: number = Number.MAX_VALUE -): Promise { - const value = await func(); - if (value) { - return value; - } else { - const promise = new Promise(resolve => { - const retry = () => { - func().then(v => { - if (v) { - resolve(v); - } else { - retries--; - if (retries > 0) { - setTimeout(retry, intervalMs); - } else { - resolve(v); - } - } - }); - }; - setTimeout(retry, intervalMs); - }); - return await promise; - } -} - -export function init(server: Server, options: any) { - if (!options.ui.enabled) { - return; - } - - const log = new Logger(server); - const serverOptions = new ServerOptions(options, server.config()); - const xpackMainPlugin: XPackMainPlugin = server.plugins.xpack_main; - xpackMainPlugin.registerFeature({ - id: 'code', - name: i18n.translate('xpack.code.featureRegistry.codeFeatureName', { - defaultMessage: 'Code', - }), - icon: 'codeApp', - navLinkId: 'code', - app: ['code', 'kibana'], - catalogue: [], // TODO add catalogue here - privileges: { - all: { - api: ['code_user', 'code_admin'], - savedObject: { - all: [], - read: ['config'], - }, - ui: ['show', 'user', 'admin'], - }, - read: { - api: ['code_user'], - savedObject: { - all: [], - read: ['config'], - }, - ui: ['show', 'user'], - }, - }, - }); - - // @ts-ignore - const kbnServer = this.kbnServer; - kbnServer.ready().then(async () => { - const codeNodeUrl = serverOptions.codeNodeUrl; - const rndString = crypto.randomBytes(20).toString('hex'); - checkRoute(server, rndString); - if (codeNodeUrl) { - const checkResult = await retryUntilAvailable( - async () => await checkCodeNode(codeNodeUrl, log, rndString), - 5000 - ); - if (checkResult.me) { - await initCodeNode(server, serverOptions, log); - } else { - await initNonCodeNode(codeNodeUrl, server, log); - } - } else { - // codeNodeUrl not set, single node mode - await initCodeNode(server, serverOptions, log); - } - }); -} - -async function initNonCodeNode(url: string, server: Server, log: Logger) { - log.info(`Initializing Code plugin as non-code node, redirecting all code requests to ${url}`); - redirectRoute(server, url, log); -} - -async function initCodeNode(server: Server, serverOptions: ServerOptions, log: Logger) { - // wait until elasticsearch is ready - // @ts-ignore - await server.plugins.elasticsearch.waitUntilReady(); - - log.info('Initializing Code plugin as code-node.'); - const queueIndex: string = server.config().get('xpack.code.queueIndex'); - const queueTimeoutMs: number = server.config().get('xpack.code.queueTimeoutMs'); - const devMode: boolean = server.config().get('env.dev'); - - const esClient: EsClient = new EsClientWithInternalRequest(server); - const repoConfigController = new RepositoryConfigController(esClient); - - server.injectUiAppVars('code', () => ({ - enableLangserversDeveloping: devMode, - })); - // Enable the developing language servers in development mode. - if (devMode) { - JAVA.downloadUrl = _.partialRight(JAVA!.downloadUrl!, devMode); - } - - // Initialize git operations - const gitOps = new GitOperations(serverOptions.repoPath); - - const installManager = new InstallManager(server, serverOptions); - const lspService = new LspService( - '127.0.0.1', - serverOptions, - gitOps, - esClient, - installManager, - new ServerLoggerFactory(server), - repoConfigController - ); - server.events.on('stop', async () => { - log.debug('shutdown lsp process'); - await lspService.shutdown(); - }); - // Initialize indexing factories. - const lspIndexerFactory = new LspIndexerFactory(lspService, serverOptions, gitOps, esClient, log); - - const repoIndexInitializerFactory = new RepositoryIndexInitializerFactory(esClient, log); - - // Initialize queue worker cancellation service. - const cancellationService = new CancellationSerivce(); - - // Execute index version checking and try to migrate index data if necessary. - await tryMigrateIndices(esClient, log); - - // Initialize queue. - const queue = new Esqueue(queueIndex, { - client: esClient, - timeout: queueTimeoutMs, - }); - const indexWorker = new IndexWorker( - queue, - log, - esClient, - [lspIndexerFactory], - gitOps, - cancellationService - ).bind(); - - const repoServiceFactory: RepositoryServiceFactory = new RepositoryServiceFactory(); - - const cloneWorker = new CloneWorker( - queue, - log, - esClient, - serverOptions, - gitOps, - indexWorker, - repoServiceFactory, - cancellationService - ).bind(); - const deleteWorker = new DeleteWorker( - queue, - log, - esClient, - serverOptions, - gitOps, - cancellationService, - lspService, - repoServiceFactory - ).bind(); - const updateWorker = new UpdateWorker( - queue, - log, - esClient, - serverOptions, - gitOps, - repoServiceFactory, - cancellationService - ).bind(); - - // Initialize schedulers. - const cloneScheduler = new CloneScheduler(cloneWorker, serverOptions, esClient, log); - const updateScheduler = new UpdateScheduler(updateWorker, serverOptions, esClient, log); - const indexScheduler = new IndexScheduler(indexWorker, serverOptions, esClient, log); - updateScheduler.start(); - if (!serverOptions.disableIndexScheduler) { - indexScheduler.start(); - } - // Check if the repository is local on the file system. - // This should be executed once at the startup time of Kibana. - cloneScheduler.schedule(); - - const codeServerRouter = new CodeServerRouter(server); - // Add server routes and initialize the plugin here - repositoryRoute( - codeServerRouter, - cloneWorker, - deleteWorker, - indexWorker, - repoIndexInitializerFactory, - repoConfigController, - serverOptions - ); - repositorySearchRoute(codeServerRouter, log); - documentSearchRoute(codeServerRouter, log); - symbolSearchRoute(codeServerRouter, log); - fileRoute(codeServerRouter, gitOps); - workspaceRoute(codeServerRouter, serverOptions, gitOps); - symbolByQnameRoute(codeServerRouter, log); - installRoute(codeServerRouter, lspService); - lspRoute(codeServerRouter, lspService, serverOptions); - setupRoute(codeServerRouter); - statusRoute(codeServerRouter, gitOps, lspService); - - server.events.on('stop', () => { - gitOps.cleanAllRepo(); - if (!serverOptions.disableIndexScheduler) { - indexScheduler.stop(); - } - updateScheduler.stop(); - queue.destroy(); - }); -} diff --git a/x-pack/legacy/plugins/code/server/init_es.ts b/x-pack/legacy/plugins/code/server/init_es.ts new file mode 100644 index 000000000000..39ae05bf2687 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/init_es.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import { RepositoryIndexInitializerFactory } from './indexer'; +import { RepositoryConfigController } from './repository_config_controller'; +import { EsClientWithInternalRequest } from './utils/esclient_with_internal_request'; +import { EsClient } from './lib/esqueue'; +import { Logger } from './log'; + +export async function initEs(server: Server, log: Logger) { + // wait until elasticsearch is ready + await server.plugins.elasticsearch.waitUntilReady(); + const esClient: EsClient = new EsClientWithInternalRequest(server); + const repoConfigController = new RepositoryConfigController(esClient); + const repoIndexInitializerFactory = new RepositoryIndexInitializerFactory(esClient, log); + return { + esClient, + repoConfigController, + repoIndexInitializerFactory, + }; +} diff --git a/x-pack/legacy/plugins/code/server/init_local.ts b/x-pack/legacy/plugins/code/server/init_local.ts new file mode 100644 index 000000000000..f39726c5e55e --- /dev/null +++ b/x-pack/legacy/plugins/code/server/init_local.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 { Server } from 'hapi'; +import { ServerOptions } from './server_options'; +import { CodeServices } from './distributed/code_services'; +import { EsClient } from './lib/esqueue'; +import { RepositoryConfigController } from './repository_config_controller'; +import { GitOperations } from './git_operations'; +import { Logger } from './log'; +import { + getGitServiceHandler, + getLspServiceHandler, + getWorkspaceHandler, + GitServiceDefinition, + GitServiceDefinitionOption, + LspServiceDefinition, + LspServiceDefinitionOption, + SetupDefinition, + setupServiceHandler, + WorkspaceDefinition, +} from './distributed/apis'; +import { InstallManager } from './lsp/install_manager'; +import { LspService } from './lsp/lsp_service'; +import { ServerLoggerFactory } from './utils/server_logger_factory'; + +export function initLocalService( + server: Server, + log: Logger, + serverOptions: ServerOptions, + codeServices: CodeServices, + esClient: EsClient, + repoConfigController: RepositoryConfigController +) { + // Initialize git operations + const gitOps = new GitOperations(serverOptions.repoPath); + codeServices.registerHandler( + GitServiceDefinition, + getGitServiceHandler(gitOps), + GitServiceDefinitionOption + ); + + const installManager = new InstallManager(server, serverOptions); + const lspService = new LspService( + '127.0.0.1', + serverOptions, + gitOps, + esClient, + installManager, + new ServerLoggerFactory(server), + repoConfigController + ); + server.events.on('stop', async () => { + log.debug('shutdown lsp process'); + await lspService.shutdown(); + await gitOps.cleanAllRepo(); + }); + codeServices.registerHandler( + LspServiceDefinition, + getLspServiceHandler(lspService), + LspServiceDefinitionOption + ); + codeServices.registerHandler( + WorkspaceDefinition, + getWorkspaceHandler(server, lspService.workspaceHandler) + ); + codeServices.registerHandler(SetupDefinition, setupServiceHandler); + + return { gitOps, lspService, installManager }; +} diff --git a/x-pack/legacy/plugins/code/server/init_queue.ts b/x-pack/legacy/plugins/code/server/init_queue.ts new file mode 100644 index 000000000000..444eb589d530 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/init_queue.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. + */ + +import { Server } from 'hapi'; +import { EsClient, Esqueue } from './lib/esqueue'; +import { Logger } from './log'; + +export function initQueue(server: Server, log: Logger, esClient: EsClient) { + const queueIndex: string = server.config().get('xpack.code.queueIndex'); + const queueTimeoutMs: number = server.config().get('xpack.code.queueTimeoutMs'); + const queue = new Esqueue(queueIndex, { + client: esClient, + timeout: queueTimeoutMs, + }); + return queue; +} diff --git a/x-pack/legacy/plugins/code/server/init_workers.ts b/x-pack/legacy/plugins/code/server/init_workers.ts new file mode 100644 index 000000000000..760e01876af7 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/init_workers.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 { Server } from 'hapi'; +import { DiskWatermarkService } from './disk_watermark'; +import { EsClient, Esqueue } from './lib/esqueue'; +import { LspService } from './lsp/lsp_service'; +import { GitOperations } from './git_operations'; +import { ServerOptions } from './server_options'; +import { CodeServices } from './distributed/code_services'; +import { LspIndexerFactory } from './indexer'; +import { CancellationSerivce, CloneWorker, DeleteWorker, IndexWorker, UpdateWorker } from './queue'; +import { RepositoryServiceFactory } from './repository_service_factory'; +import { getRepositoryHandler, RepositoryServiceDefinition } from './distributed/apis'; +import { CloneScheduler, IndexScheduler, UpdateScheduler } from './scheduler'; +import { Logger } from './log'; + +export function initWorkers( + server: Server, + log: Logger, + esClient: EsClient, + queue: Esqueue, + lspService: LspService, + gitOps: GitOperations, + serverOptions: ServerOptions, + codeServices: CodeServices +) { + // Initialize indexing factories. + const lspIndexerFactory = new LspIndexerFactory(lspService, serverOptions, gitOps, esClient, log); + + // Initialize queue worker cancellation service. + const cancellationService = new CancellationSerivce(); + const indexWorker = new IndexWorker( + queue, + log, + esClient, + [lspIndexerFactory], + gitOps, + cancellationService + ).bind(codeServices); + + const repoServiceFactory: RepositoryServiceFactory = new RepositoryServiceFactory(); + + const watermarkService = new DiskWatermarkService( + serverOptions.disk.watermarkLowMb, + serverOptions.repoPath + ); + const cloneWorker = new CloneWorker( + queue, + log, + esClient, + serverOptions, + gitOps, + indexWorker, + repoServiceFactory, + cancellationService, + watermarkService + ).bind(codeServices); + const deleteWorker = new DeleteWorker( + queue, + log, + esClient, + serverOptions, + gitOps, + cancellationService, + lspService, + repoServiceFactory + ).bind(codeServices); + const updateWorker = new UpdateWorker( + queue, + log, + esClient, + serverOptions, + gitOps, + repoServiceFactory, + cancellationService, + watermarkService + ).bind(codeServices); + codeServices.registerHandler( + RepositoryServiceDefinition, + getRepositoryHandler(cloneWorker, deleteWorker, indexWorker) + ); + + // Initialize schedulers. + const cloneScheduler = new CloneScheduler(cloneWorker, serverOptions, esClient, log); + const updateScheduler = new UpdateScheduler(updateWorker, serverOptions, esClient, log); + const indexScheduler = new IndexScheduler(indexWorker, serverOptions, esClient, log); + updateScheduler.start(); + indexScheduler.start(); + // Check if the repository is local on the file system. + // This should be executed once at the startup time of Kibana. + cloneScheduler.schedule(); + return { indexScheduler, updateScheduler }; +} diff --git a/x-pack/legacy/plugins/code/server/lib/esqueue/worker.js b/x-pack/legacy/plugins/code/server/lib/esqueue/worker.js index 3713027b36c6..5176cdd2ffd1 100644 --- a/x-pack/legacy/plugins/code/server/lib/esqueue/worker.js +++ b/x-pack/legacy/plugins/code/server/lib/esqueue/worker.js @@ -36,6 +36,7 @@ export class Worker extends events.EventEmitter { this.id = puid.generate(); this.queue = queue; this.client = opts.client || this.queue.client; + this.codeServices = opts.codeServices; this.jobtype = type; this.workerFn = workerFn; this.checkSize = opts.size || 10; @@ -465,7 +466,11 @@ export class Worker extends events.EventEmitter { if (jobs.length > 0) { this.debug(`${jobs.length} outstanding jobs returned`); } - return jobs; + return jobs.filter(async job => { + const payload = job._source.payload.payload; + const repoUrl = payload.uri || payload.url; + return await this.codeServices.isResourceLocal(repoUrl); + }); }) .catch((err) => { // ignore missing indices errors diff --git a/x-pack/legacy/plugins/code/server/log.ts b/x-pack/legacy/plugins/code/server/log.ts index 483f304d80bc..352e649fb752 100644 --- a/x-pack/legacy/plugins/code/server/log.ts +++ b/x-pack/legacy/plugins/code/server/log.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; import { inspect } from 'util'; import { Logger as VsLogger } from 'vscode-jsonrpc'; +import { ServerFacade } from '..'; + export class Logger implements VsLogger { private readonly verbose: boolean = false; - constructor(private server: Hapi.Server, private baseTags: string[] = ['code']) { + constructor(private server: ServerFacade, private baseTags: string[] = ['code']) { if (server) { this.verbose = this.server.config().get('xpack.code.verbose'); } diff --git a/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.ts b/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.ts index 9951b90e52d9..cf861f100921 100644 --- a/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.ts +++ b/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import fs from 'fs'; import { ChildProcess } from 'child_process'; +import fs from 'fs'; import { ResponseError } from 'vscode-jsonrpc'; -import { ILanguageServerLauncher } from './language_server_launcher'; +import { LanguageServerStartFailed } from '../../common/lsp_error_codes'; +import { Logger } from '../log'; import { ServerOptions } from '../server_options'; import { LoggerFactory } from '../utils/log_factory'; -import { Logger } from '../log'; +import { ILanguageServerLauncher } from './language_server_launcher'; import { LanguageServerProxy } from './proxy'; import { RequestExpander } from './request_expander'; -import { LanguageServerStartFailed } from '../../common/lsp_error_codes'; let seqNo = 1; diff --git a/x-pack/legacy/plugins/code/server/lsp/controller.ts b/x-pack/legacy/plugins/code/server/lsp/controller.ts index 30eb3613cafc..d7c4a44b27f6 100644 --- a/x-pack/legacy/plugins/code/server/lsp/controller.ts +++ b/x-pack/legacy/plugins/code/server/lsp/controller.ts @@ -46,7 +46,7 @@ export class LanguageServerController implements ILanguageServerHandler { // a list of support language servers private readonly languageServers: LanguageServerData[]; // a { lang -> server } map from above list - private readonly languageServerMap: { [lang: string]: LanguageServerData }; + private readonly languageServerMap: { [lang: string]: LanguageServerData[] }; private log: Logger; constructor( @@ -64,13 +64,22 @@ export class LanguageServerController implements ILanguageServerHandler { maxWorkspace: options.maxWorkspace, launcher: new def.launcher(this.targetHost, options, loggerFactory), })); + const add2map = ( + map: { [lang: string]: LanguageServerData[] }, + lang: string, + ls: LanguageServerData + ) => { + const arr = map[lang] || []; + arr.push(ls); + map[lang] = arr.sort((a, b) => b.definition.priority - a.definition.priority); + }; this.languageServerMap = this.languageServers.reduce( (map, ls) => { - ls.languages.forEach(lang => (map[lang] = ls)); - map[ls.definition.name] = ls; + ls.languages.forEach(lang => add2map(map, lang, ls)); + map[ls.definition.name] = [ls]; return map; }, - {} as { [lang: string]: LanguageServerData } + {} as { [lang: string]: LanguageServerData[] } ); } @@ -183,24 +192,24 @@ export class LanguageServerController implements ILanguageServerHandler { } } - public status(lang: string): LanguageServerStatus { - const ls = this.languageServerMap[lang]; - const status = this.installManager.status(ls.definition); + public status(def: LanguageServerDefinition): LanguageServerStatus { + const status = this.installManager.status(def); // installed, but is it running? if (status === LanguageServerStatus.READY) { - if (ls.launcher.running) { + const ls = this.languageServers.find(d => d.definition === def); + if (ls && ls.launcher.running) { return LanguageServerStatus.RUNNING; } } return status; } - public getLanguageServerDef(lang: string): LanguageServerDefinition | null { + public getLanguageServerDef(lang: string): LanguageServerDefinition[] { const data = this.languageServerMap[lang]; if (data) { - return data.definition; + return data.map(d => d.definition); } - return null; + return []; } private async findOrCreateHandler( @@ -250,18 +259,18 @@ export class LanguageServerController implements ILanguageServerHandler { } private findLanguageServer(lang: string) { - const ls = this.languageServerMap[lang]; - if (ls) { - if ( - !this.options.lsp.detach && - this.installManager.status(ls.definition) !== LanguageServerStatus.READY - ) { + const lsArr = this.languageServerMap[lang]; + if (lsArr) { + const ls = lsArr.find( + d => this.installManager.status(d.definition) !== LanguageServerStatus.NOT_INSTALLED + ); + if (!this.options.lsp.detach && ls === undefined) { throw new ResponseError( LanguageServerNotInstalled, `language server ${lang} not installed` ); } else { - return ls; + return ls!; } } else { throw new ResponseError(UnknownFileLanguage, `unsupported language ${lang}`); diff --git a/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts b/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts index eb26d479f7e1..0b4e2991f28a 100644 --- a/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts +++ b/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ChildProcess } from 'child_process'; +import { spawn } from 'child_process'; +import fs from 'fs'; import getPort from 'get-port'; -import { Logger, MarkupKind } from 'vscode-languageserver-protocol'; +import * as glob from 'glob'; +import path from 'path'; +import { MarkupKind } from 'vscode-languageserver-protocol'; +import { Logger } from '../log'; import { ServerOptions } from '../server_options'; import { LoggerFactory } from '../utils/log_factory'; import { AbstractLauncher } from './abstract_launcher'; @@ -24,13 +28,6 @@ export class GoServerLauncher extends AbstractLauncher { super('go', targetHost, options, loggerFactory); } - async getPort() { - if (!this.options.lsp.detach) { - return await getPort(); - } - return GO_LANG_DETACH_PORT; - } - createExpander( proxy: LanguageServerProxy, builtinWorkspace: boolean, @@ -53,8 +50,76 @@ export class GoServerLauncher extends AbstractLauncher { this.log ); } - // TODO(henrywong): Once go langugage server ready to release, we should support this mode. - async spawnProcess(installationPath: string, port: number, log: Logger): Promise { - throw new Error('Go language server currently only support detach mode'); + + async startConnect(proxy: LanguageServerProxy) { + await proxy.connect(); + } + + async getPort() { + if (!this.options.lsp.detach) { + return await getPort(); + } + return GO_LANG_DETACH_PORT; + } + + private async getBundledGoToolchain(installationPath: string, log: Logger) { + const GoToolchain = glob.sync('**/go/**', { + cwd: installationPath, + }); + if (!GoToolchain.length) { + return undefined; + } + return path.resolve(installationPath, GoToolchain[0]); + } + + async spawnProcess(installationPath: string, port: number, log: Logger) { + const launchersFound = glob.sync('go-langserver', { + cwd: installationPath, + }); + if (!launchersFound.length) { + throw new Error('Cannot find executable go language server'); + } + + let envPath = process.env.PATH; + const goToolchain = await this.getBundledGoToolchain(installationPath, log); + if (!goToolchain) { + throw new Error('Cannot find go toolchain in bundle installation'); + } + // Construct $GOROOT from the bundled go toolchain. + const goRoot = goToolchain; + const goHome = path.resolve(goToolchain, 'bin'); + envPath = envPath + ':' + goHome; + // Construct $GOPATH under 'kibana/data/code'. + const goPath = this.options.goPath; + if (!fs.existsSync(goPath)) { + fs.mkdirSync(goPath); + } + + const params: string[] = ['-port=' + port.toString()]; + const golsp = path.resolve(installationPath, launchersFound[0]); + const p = spawn(golsp, params, { + detached: false, + stdio: 'pipe', + env: { + ...process.env, + CLIENT_HOST: '127.0.0.1', + CLIENT_PORT: port.toString(), + GOROOT: goRoot, + GOPATH: goPath, + PATH: envPath, + GO111MODULE: 'on', + CGO_ENABLED: '0', + }, + }); + p.stdout.on('data', data => { + log.stdout(data.toString()); + }); + p.stderr.on('data', data => { + log.stderr(data.toString()); + }); + log.info( + `Launch Go Language Server at port ${port.toString()}, pid:${p.pid}, GOROOT:${goRoot}` + ); + return p; } } diff --git a/x-pack/legacy/plugins/code/server/lsp/install_manager.test.ts b/x-pack/legacy/plugins/code/server/lsp/install_manager.test.ts index 9ce9a024a38e..6aa80899a700 100644 --- a/x-pack/legacy/plugins/code/server/lsp/install_manager.test.ts +++ b/x-pack/legacy/plugins/code/server/lsp/install_manager.test.ts @@ -6,15 +6,17 @@ /* eslint-disable no-console */ import fs from 'fs'; +import { Server } from 'hapi'; import os from 'os'; import path from 'path'; +import rimraf from 'rimraf'; + import { LanguageServers } from './language_servers'; import { InstallManager } from './install_manager'; import { ServerOptions } from '../server_options'; -import rimraf from 'rimraf'; import { LanguageServerStatus } from '../../common/language_server'; -import { Server } from 'hapi'; import { InstallationType } from '../../common/installation'; +import { ServerFacade } from '../..'; const LANG_SERVER_NAME = 'Java'; const langSrvDef = LanguageServers.find(l => l.name === LANG_SERVER_NAME)!; @@ -23,7 +25,7 @@ const fakeTestDir = path.join(os.tmpdir(), 'foo-'); const options: ServerOptions = {} as ServerOptions; -const server = new Server(); +const server: ServerFacade = new Server(); server.config = () => { return { get(key: string): any { diff --git a/x-pack/legacy/plugins/code/server/lsp/install_manager.ts b/x-pack/legacy/plugins/code/server/lsp/install_manager.ts index 70de222f16f6..f75cd2e40199 100644 --- a/x-pack/legacy/plugins/code/server/lsp/install_manager.ts +++ b/x-pack/legacy/plugins/code/server/lsp/install_manager.ts @@ -5,14 +5,15 @@ */ import fs from 'fs'; -import { Server } from 'hapi'; + import { InstallationType } from '../../common/installation'; import { LanguageServerStatus } from '../../common/language_server'; import { ServerOptions } from '../server_options'; import { LanguageServerDefinition } from './language_servers'; +import { ServerFacade } from '../..'; export class InstallManager { - constructor(public readonly server: Server, readonly serverOptions: ServerOptions) {} + constructor(public readonly server: ServerFacade, readonly serverOptions: ServerOptions) {} public status(def: LanguageServerDefinition): LanguageServerStatus { if (def.installationType === InstallationType.Embed) { diff --git a/x-pack/legacy/plugins/code/server/lsp/java_launcher.ts b/x-pack/legacy/plugins/code/server/lsp/java_launcher.ts index 4099398065b1..82ba16db15ac 100644 --- a/x-pack/legacy/plugins/code/server/lsp/java_launcher.ts +++ b/x-pack/legacy/plugins/code/server/lsp/java_launcher.ts @@ -43,6 +43,13 @@ export class JavaLauncher extends AbstractLauncher { 'java.autobuild.enabled': false, }, }, + clientCapabilities: { + textDocument: { + documentSymbol: { + hierarchicalDocumentSymbolSupport: true, + }, + }, + }, } as InitializeOptions, this.log ); diff --git a/x-pack/legacy/plugins/code/server/lsp/language_servers.ts b/x-pack/legacy/plugins/code/server/lsp/language_servers.ts index c4f06928bff1..d30f48638b84 100644 --- a/x-pack/legacy/plugins/code/server/lsp/language_servers.ts +++ b/x-pack/legacy/plugins/code/server/lsp/language_servers.ts @@ -4,16 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; - +import { ServerFacade } from '../..'; import { InstallationType } from '../../common/installation'; -import { LanguageServer } from '../../common/language_server'; +import { CTAGS_SUPPORT_LANGS, LanguageServer } from '../../common/language_server'; import { CtagsLauncher } from './ctags_launcher'; import { GoServerLauncher } from './go_launcher'; import { JavaLauncher } from './java_launcher'; import { LauncherConstructor } from './language_server_launcher'; import { TypescriptServerLauncher } from './ts_launcher'; -import { CTAGS_SUPPORT_LANGS } from '../../common/language_server'; export interface LanguageServerDefinition extends LanguageServer { builtinWorkspaceFolders: boolean; @@ -22,6 +20,7 @@ export interface LanguageServerDefinition extends LanguageServer { downloadUrl?: (version: string, devMode?: boolean) => string; embedPath?: string; installationPluginName?: string; + priority: number; } export const TYPESCRIPT: LanguageServerDefinition = { @@ -31,6 +30,7 @@ export const TYPESCRIPT: LanguageServerDefinition = { launcher: TypescriptServerLauncher, installationType: InstallationType.Embed, embedPath: require.resolve('@elastic/javascript-typescript-langserver/lib/language-server.js'), + priority: 2, }; export const JAVA: LanguageServerDefinition = { name: 'Java', @@ -40,6 +40,7 @@ export const JAVA: LanguageServerDefinition = { installationType: InstallationType.Plugin, installationPluginName: 'java-langserver', installationFolderName: 'jdt', + priority: 2, downloadUrl: (version: string, devMode?: boolean) => devMode! ? `https://snapshots.elastic.co/downloads/java-langserver-plugins/java-langserver/java-langserver-${version}-SNAPSHOT-$OS.zip` @@ -51,7 +52,13 @@ export const GO: LanguageServerDefinition = { languages: ['go'], launcher: GoServerLauncher, installationType: InstallationType.Plugin, - installationPluginName: 'goLanguageServer', + installationPluginName: 'go-langserver', + priority: 2, + installationFolderName: 'golsp', + downloadUrl: (version: string, devMode?: boolean) => + devMode! + ? `https://snapshots.elastic.co/downloads/go-langserver-plugins/go-langserver/go-langserver-${version}-SNAPSHOT-$OS.zip` + : `https://artifacts.elastic.co/downloads/go-langserver-plugins/go-langserver/go-langserver-${version}-$OS.zip`, }; export const CTAGS: LanguageServerDefinition = { name: 'Ctags', @@ -60,11 +67,12 @@ export const CTAGS: LanguageServerDefinition = { launcher: CtagsLauncher, installationType: InstallationType.Embed, embedPath: require.resolve('@elastic/ctags-langserver/lib/cli.js'), + priority: 1, }; -export const LanguageServers: LanguageServerDefinition[] = [TYPESCRIPT, JAVA, CTAGS]; -export const LanguageServersDeveloping: LanguageServerDefinition[] = [GO]; +export const LanguageServers: LanguageServerDefinition[] = [TYPESCRIPT, JAVA, CTAGS, GO]; +export const LanguageServersDeveloping: LanguageServerDefinition[] = []; -export function enabledLanguageServers(server: Hapi.Server) { +export function enabledLanguageServers(server: ServerFacade) { const devMode: boolean = server.config().get('env.dev'); function isEnabled(lang: LanguageServerDefinition, defaultEnabled: boolean) { diff --git a/x-pack/legacy/plugins/code/server/lsp/lsp_service.ts b/x-pack/legacy/plugins/code/server/lsp/lsp_service.ts index 5da93c2f3492..a7b54740d635 100644 --- a/x-pack/legacy/plugins/code/server/lsp/lsp_service.ts +++ b/x-pack/legacy/plugins/code/server/lsp/lsp_service.ts @@ -79,15 +79,21 @@ export class LspService { } public supportLanguage(lang: string) { - return this.controller.getLanguageServerDef(lang) !== null; + return this.controller.getLanguageServerDef(lang).length > 0; } public getLanguageSeverDef(lang: string) { return this.controller.getLanguageServerDef(lang); } - public languageServerStatus(lang: string): LanguageServerStatus { - return this.controller.status(lang); + public languageServerStatus(name: string): LanguageServerStatus { + const defs = this.controller.getLanguageServerDef(name); + if (defs.length > 0) { + const def = defs[0]; + return this.controller.status(def); + } else { + return LanguageServerStatus.NOT_INSTALLED; + } } public async initializeState(repoUri: string, revision: string) { diff --git a/x-pack/legacy/plugins/code/server/plugin.ts b/x-pack/legacy/plugins/code/server/plugin.ts new file mode 100644 index 000000000000..408afa1eddee --- /dev/null +++ b/x-pack/legacy/plugins/code/server/plugin.ts @@ -0,0 +1,272 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 crypto from 'crypto'; +import * as _ from 'lodash'; +import { CoreSetup, PluginInitializerContext } from 'src/core/server'; + +import { XPackMainPlugin } from '../../xpack_main/xpack_main'; +import { GitOperations } from './git_operations'; +import { RepositoryIndexInitializerFactory, tryMigrateIndices } from './indexer'; +import { Esqueue } from './lib/esqueue'; +import { Logger } from './log'; +import { JAVA } from './lsp/language_servers'; +import { LspService } from './lsp/lsp_service'; +import { RepositoryConfigController } from './repository_config_controller'; +import { IndexScheduler, UpdateScheduler } from './scheduler'; +import { CodeServerRouter } from './security'; +import { ServerOptions } from './server_options'; +import { + checkCodeNode, + checkRoute, + documentSearchRoute, + fileRoute, + installRoute, + lspRoute, + repositoryRoute, + repositorySearchRoute, + setupRoute, + statusRoute, + symbolByQnameRoute, + symbolSearchRoute, + workspaceRoute, +} from './routes'; +import { CodeServices } from './distributed/code_services'; +import { CodeNodeAdapter } from './distributed/multinode/code_node_adapter'; +import { LocalHandlerAdapter } from './distributed/local_handler_adapter'; +import { NonCodeNodeAdapter } from './distributed/multinode/non_code_node_adapter'; +import { + GitServiceDefinition, + GitServiceDefinitionOption, + LspServiceDefinition, + LspServiceDefinitionOption, + RepositoryServiceDefinition, + SetupDefinition, + WorkspaceDefinition, +} from './distributed/apis'; +import { initEs } from './init_es'; +import { initLocalService } from './init_local'; +import { initQueue } from './init_queue'; +import { initWorkers } from './init_workers'; + +export class CodePlugin { + private isCodeNode = false; + + private gitOps: GitOperations | null = null; + private queue: Esqueue | null = null; + private log: Logger; + private serverOptions: ServerOptions; + private indexScheduler: IndexScheduler | null = null; + private updateScheduler: UpdateScheduler | null = null; + private lspService: LspService | null = null; + + constructor(initializerContext: PluginInitializerContext) { + this.log = {} as Logger; + this.serverOptions = {} as ServerOptions; + } + + // TODO: options is not a valid param for the setup() api + // of the new platform. Will need to pass through the configs + // correctly in the new platform. + public setup(core: CoreSetup, options: any) { + const { server } = core.http as any; + + this.log = new Logger(server); + this.serverOptions = new ServerOptions(options, server.config()); + + const xpackMainPlugin: XPackMainPlugin = server.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + id: 'code', + name: i18n.translate('xpack.code.featureRegistry.codeFeatureName', { + defaultMessage: 'Code', + }), + icon: 'codeApp', + navLinkId: 'code', + app: ['code', 'kibana'], + catalogue: [], // TODO add catalogue here + privileges: { + all: { + api: ['code_user', 'code_admin'], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show', 'user', 'admin'], + }, + read: { + api: ['code_user'], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show', 'user'], + }, + }, + }); + } + + // TODO: CodeStart will not have the register route api. + // Let's make it CoreSetup as the param for now. + public async start(core: CoreSetup) { + // called after all plugins are set up + const { server } = core.http as any; + const codeServerRouter = new CodeServerRouter(server); + const codeNodeUrl = this.serverOptions.codeNodeUrl; + const rndString = crypto.randomBytes(20).toString('hex'); + checkRoute(server, rndString); + if (codeNodeUrl) { + const checkResult = await this.retryUntilAvailable( + async () => await checkCodeNode(codeNodeUrl, this.log, rndString), + 5000 + ); + if (checkResult.me) { + const codeServices = new CodeServices(new CodeNodeAdapter(codeServerRouter, this.log)); + this.log.info('Initializing Code plugin as code-node.'); + await this.initCodeNode(server, codeServices); + } else { + await this.initNonCodeNode(codeNodeUrl, core); + } + } else { + const codeServices = new CodeServices(new LocalHandlerAdapter()); + // codeNodeUrl not set, single node mode + this.log.info('Initializing Code plugin as single-node.'); + this.initDevMode(server); + await this.initCodeNode(server, codeServices); + } + } + + private async initCodeNode(server: any, codeServices: CodeServices) { + this.isCodeNode = true; + const { esClient, repoConfigController, repoIndexInitializerFactory } = await initEs( + server, + this.log + ); + + this.queue = initQueue(server, this.log, esClient); + + const { gitOps, lspService } = initLocalService( + server, + this.log, + this.serverOptions, + codeServices, + esClient, + repoConfigController + ); + this.lspService = lspService; + this.gitOps = gitOps; + const { indexScheduler, updateScheduler } = initWorkers( + server, + this.log, + esClient, + this.queue!, + lspService, + gitOps, + this.serverOptions, + codeServices + ); + this.indexScheduler = indexScheduler; + this.updateScheduler = updateScheduler; + + // Execute index version checking and try to migrate index data if necessary. + await tryMigrateIndices(esClient, this.log); + + this.initRoutes(server, codeServices, repoIndexInitializerFactory, repoConfigController); + } + + public async stop() { + if (this.isCodeNode) { + if (this.gitOps) await this.gitOps.cleanAllRepo(); + if (this.indexScheduler) this.indexScheduler.stop(); + if (this.updateScheduler) this.updateScheduler.stop(); + if (this.queue) this.queue.destroy(); + if (this.lspService) await this.lspService.shutdown(); + } + } + + private async initNonCodeNode(url: string, core: CoreSetup) { + const { server } = core.http as any; + this.log.info( + `Initializing Code plugin as non-code node, redirecting all code requests to ${url}` + ); + const codeServices = new CodeServices(new NonCodeNodeAdapter(url, this.log)); + codeServices.registerHandler(GitServiceDefinition, null, GitServiceDefinitionOption); + codeServices.registerHandler(RepositoryServiceDefinition, null); + codeServices.registerHandler(LspServiceDefinition, null, LspServiceDefinitionOption); + codeServices.registerHandler(WorkspaceDefinition, null); + codeServices.registerHandler(SetupDefinition, null); + const { repoConfigController, repoIndexInitializerFactory } = await initEs(server, this.log); + this.initRoutes(server, codeServices, repoIndexInitializerFactory, repoConfigController); + } + + private async initRoutes( + server: any, + codeServices: CodeServices, + repoIndexInitializerFactory: RepositoryIndexInitializerFactory, + repoConfigController: RepositoryConfigController + ) { + const codeServerRouter = new CodeServerRouter(server); + repositoryRoute( + codeServerRouter, + codeServices, + repoIndexInitializerFactory, + repoConfigController, + this.serverOptions + ); + repositorySearchRoute(codeServerRouter, this.log); + documentSearchRoute(codeServerRouter, this.log); + symbolSearchRoute(codeServerRouter, this.log); + fileRoute(codeServerRouter, codeServices); + workspaceRoute(codeServerRouter, this.serverOptions, codeServices); + symbolByQnameRoute(codeServerRouter, this.log); + installRoute(codeServerRouter, codeServices); + lspRoute(codeServerRouter, codeServices, this.serverOptions); + setupRoute(codeServerRouter, codeServices); + statusRoute(codeServerRouter, codeServices); + } + + private async retryUntilAvailable( + func: () => Promise, + intervalMs: number, + retries: number = Number.MAX_VALUE + ): Promise { + const value = await func(); + if (value) { + return value; + } else { + const promise = new Promise(resolve => { + const retry = () => { + func().then(v => { + if (v) { + resolve(v); + } else { + retries--; + if (retries > 0) { + setTimeout(retry, intervalMs); + } else { + resolve(v); + } + } + }); + }; + setTimeout(retry, intervalMs); + }); + return await promise; + } + } + + private initDevMode(server: any) { + // @ts-ignore + const devMode: boolean = server.config().get('env.dev'); + server.injectUiAppVars('code', () => ({ + enableLangserversDeveloping: devMode, + })); + // Enable the developing language servers in development mode. + if (devMode) { + JAVA.downloadUrl = _.partialRight(JAVA!.downloadUrl!, devMode); + } + } +} diff --git a/x-pack/legacy/plugins/code/server/queue/abstract_git_worker.ts b/x-pack/legacy/plugins/code/server/queue/abstract_git_worker.ts index 0d26254792f1..d5282008ad5f 100644 --- a/x-pack/legacy/plugins/code/server/queue/abstract_git_worker.ts +++ b/x-pack/legacy/plugins/code/server/queue/abstract_git_worker.ts @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + import { CloneProgress, CloneWorkerProgress, CloneWorkerResult, WorkerReservedProgress, + WorkerResult, } from '../../model'; +import { DiskWatermarkService } from '../disk_watermark'; import { GitOperations } from '../git_operations'; import { EsClient, Esqueue } from '../lib/esqueue'; import { Logger } from '../log'; @@ -27,12 +31,34 @@ export abstract class AbstractGitWorker extends AbstractWorker { protected readonly log: Logger, protected readonly client: EsClient, protected readonly serverOptions: ServerOptions, - protected readonly gitOps: GitOperations + protected readonly gitOps: GitOperations, + protected readonly watermarkService: DiskWatermarkService ) { super(queue, log); this.objectClient = new RepositoryObjectClient(client); } + public async executeJob(_: Job): Promise { + const { thresholdEnabled, watermarkLowMb } = this.serverOptions.disk; + if (thresholdEnabled) { + const isLowWatermark = await this.watermarkService.isLowWatermark(); + if (isLowWatermark) { + const msg = i18n.translate('xpack.code.git.diskWatermarkLowMessage', { + defaultMessage: `Disk watermark level lower than {watermarkLowMb} MB`, + values: { + watermarkLowMb, + }, + }); + this.log.error(msg); + throw new Error(msg); + } + } + + return new Promise((resolve, reject) => { + resolve(); + }); + } + public async onJobCompleted(job: Job, res: CloneWorkerResult) { if (res.cancelled) { // Skip updating job progress if the job is done because of cancellation. diff --git a/x-pack/legacy/plugins/code/server/queue/abstract_worker.ts b/x-pack/legacy/plugins/code/server/queue/abstract_worker.ts index b1b1d6b243f1..c74f640a1d21 100644 --- a/x-pack/legacy/plugins/code/server/queue/abstract_worker.ts +++ b/x-pack/legacy/plugins/code/server/queue/abstract_worker.ts @@ -16,6 +16,7 @@ import { import { Logger } from '../log'; import { Job } from './job'; import { Worker } from './worker'; +import { CodeServices } from '../distributed/code_services'; export abstract class AbstractWorker implements Worker { // The id of the worker. Also serves as the id of the job this worker consumes. @@ -69,7 +70,7 @@ export abstract class AbstractWorker implements Worker { }); } - public bind() { + public bind(codeServices: CodeServices) { const workerFn = (payload: any, cancellationToken: CancellationToken) => { const job: Job = { ...payload, @@ -82,6 +83,7 @@ export abstract class AbstractWorker implements Worker { interval: 5000, capacity: 5, intervalErrorMultiplier: 1, + codeServices, }; const queueWorker = this.queue.registerWorker(this.id, workerFn as any, workerOptions); diff --git a/x-pack/legacy/plugins/code/server/queue/cancellation_service.test.ts b/x-pack/legacy/plugins/code/server/queue/cancellation_service.test.ts index c1ec6240fcc1..735a32a05594 100644 --- a/x-pack/legacy/plugins/code/server/queue/cancellation_service.test.ts +++ b/x-pack/legacy/plugins/code/server/queue/cancellation_service.test.ts @@ -25,12 +25,46 @@ test('Register and cancel cancellation token', async () => { const cancelSpy = sinon.spy(); token.cancel = cancelSpy; - await service.registerCancelableIndexJob( - repoUri, - token as CancellationToken, - Promise.resolve('resolved') - ); - await service.cancelIndexJob(repoUri); + // create a promise and defer its fulfillment + let promiseResolve: () => void = () => {}; + const promise = new Promise(resolve => { + promiseResolve = resolve; + }); + await service.registerCancelableIndexJob(repoUri, token as CancellationToken, promise); + // do not wait on the promise, or there will be a dead lock + const cancelPromise = service.cancelIndexJob(repoUri); + // resolve the promise now + promiseResolve(); + + await cancelPromise; + + expect(cancelSpy.calledOnce).toBeTruthy(); +}); + +test('Register and cancel cancellation token while an exception is thrown from the job', async () => { + const repoUri = 'github.com/elastic/code'; + const service = new CancellationSerivce(); + const token = { + cancel: (): void => { + return; + }, + }; + const cancelSpy = sinon.spy(); + token.cancel = cancelSpy; + + // create a promise and defer its rejection + let promiseReject: () => void = () => {}; + const promise = new Promise((resolve, reject) => { + promiseReject = reject; + }); + await service.registerCancelableIndexJob(repoUri, token as CancellationToken, promise); + // expect no exceptions are thrown when cancelling the job + // do not wait on the promise, or there will be a dead lock + const cancelPromise = service.cancelIndexJob(repoUri); + // reject the promise now + promiseReject(); + + await cancelPromise; expect(cancelSpy.calledOnce).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/code/server/queue/cancellation_service.ts b/x-pack/legacy/plugins/code/server/queue/cancellation_service.ts index e83259c5eeef..251d767ce26d 100644 --- a/x-pack/legacy/plugins/code/server/queue/cancellation_service.ts +++ b/x-pack/legacy/plugins/code/server/queue/cancellation_service.ts @@ -68,6 +68,10 @@ export class CancellationSerivce { // Try to cancel the job first. await this.cancelJob(jobMap, repoUri); jobMap.set(repoUri, { token, jobPromise }); + // remove the record from the cancellation service when the promise is fulfilled or rejected. + jobPromise.finally(() => { + jobMap.delete(repoUri); + }); } private async cancelJob(jobMap: Map, repoUri: RepositoryUri) { @@ -77,9 +81,12 @@ export class CancellationSerivce { // 1. Use the cancellation token to pass cancel message to job token.cancel(); // 2. waiting on the actual job promise to be resolved - await jobPromise; - // 3. remove the record from the cancellation service - jobMap.delete(repoUri); + try { + await jobPromise; + } catch (e) { + // the exception from the job also indicates the job is finished, and it should be the duty of the worker for + // the job to handle it, so it's safe to just ignore the exception here + } } } } diff --git a/x-pack/legacy/plugins/code/server/queue/clone_worker.ts b/x-pack/legacy/plugins/code/server/queue/clone_worker.ts index d95b3c96f2a9..4761a2bb20b2 100644 --- a/x-pack/legacy/plugins/code/server/queue/clone_worker.ts +++ b/x-pack/legacy/plugins/code/server/queue/clone_worker.ts @@ -14,6 +14,7 @@ import { CloneWorkerResult, WorkerReservedProgress, } from '../../model'; +import { DiskWatermarkService } from '../disk_watermark'; import { GitOperations } from '../git_operations'; import { EsClient, Esqueue } from '../lib/esqueue'; import { Logger } from '../log'; @@ -35,12 +36,15 @@ export class CloneWorker extends AbstractGitWorker { protected readonly gitOps: GitOperations, private readonly indexWorker: IndexWorker, private readonly repoServiceFactory: RepositoryServiceFactory, - private readonly cancellationService: CancellationSerivce + private readonly cancellationService: CancellationSerivce, + protected readonly watermarkService: DiskWatermarkService ) { - super(queue, log, client, serverOptions, gitOps); + super(queue, log, client, serverOptions, gitOps, watermarkService); } public async executeJob(job: Job) { + await super.executeJob(job); + const { payload, cancellationToken } = job; const { url } = payload; try { diff --git a/x-pack/legacy/plugins/code/server/queue/update_worker.test.ts b/x-pack/legacy/plugins/code/server/queue/update_worker.test.ts index 82c6e640da8d..199a5f372fd7 100644 --- a/x-pack/legacy/plugins/code/server/queue/update_worker.test.ts +++ b/x-pack/legacy/plugins/code/server/queue/update_worker.test.ts @@ -8,6 +8,7 @@ import sinon from 'sinon'; import { EsClient, Esqueue } from '../lib/esqueue'; import { Repository } from '../../model'; +import { DiskWatermarkService } from '../disk_watermark'; import { GitOperations } from '../git_operations'; import { Logger } from '../log'; import { RepositoryServiceFactory } from '../repository_service_factory'; @@ -19,7 +20,10 @@ import { UpdateWorker } from './update_worker'; const log: Logger = new ConsoleLoggerFactory().getLogger(['test']); -const esClient = {}; +const esClient = { + update: emptyAsyncFunc, + get: emptyAsyncFunc, +}; const esQueue = {}; afterEach(() => { @@ -51,6 +55,12 @@ test('Execute update job', async () => { cancellationService.cancelUpdateJob = cancelUpdateJobSpy; cancellationService.registerCancelableUpdateJob = registerCancelableUpdateJobSpy; + // Setup DiskWatermarkService + const isLowWatermarkSpy = sinon.stub().resolves(false); + const diskWatermarkService: any = { + isLowWatermark: isLowWatermarkSpy, + }; + const updateWorker = new UpdateWorker( esQueue as Esqueue, log, @@ -59,10 +69,15 @@ test('Execute update job', async () => { security: { enableGitCertCheck: true, }, + disk: { + thresholdEnabled: true, + watermarkLowMb: 100, + }, } as ServerOptions, {} as GitOperations, (repoServiceFactory as any) as RepositoryServiceFactory, - cancellationService as CancellationSerivce + cancellationService as CancellationSerivce, + diskWatermarkService as DiskWatermarkService ); await updateWorker.executeJob({ @@ -73,6 +88,7 @@ test('Execute update job', async () => { timestamp: 0, }); + expect(isLowWatermarkSpy.calledOnce).toBeTruthy(); expect(newInstanceSpy.calledOnce).toBeTruthy(); expect(updateSpy.calledOnce).toBeTruthy(); }); @@ -99,10 +115,15 @@ test('On update job completed because of cancellation ', async () => { security: { enableGitCertCheck: true, }, + disk: { + thresholdEnabled: true, + watermarkLowMb: 100, + }, } as ServerOptions, {} as GitOperations, {} as RepositoryServiceFactory, - cancellationService as CancellationSerivce + cancellationService as CancellationSerivce, + {} as DiskWatermarkService ); await updateWorker.onJobCompleted( @@ -127,3 +148,122 @@ test('On update job completed because of cancellation ', async () => { // cancellation. expect(updateSpy.notCalled).toBeTruthy(); }); + +test('Execute update job failed because of low disk watermark ', async () => { + // Setup RepositoryService + const updateSpy = sinon.spy(); + const repoService = { + update: emptyAsyncFunc, + }; + repoService.update = updateSpy; + const repoServiceFactory = { + newInstance: (): void => { + return; + }, + }; + const newInstanceSpy = sinon.fake.returns(repoService); + repoServiceFactory.newInstance = newInstanceSpy; + + // Setup CancellationService + const cancelUpdateJobSpy = sinon.spy(); + const registerCancelableUpdateJobSpy = sinon.spy(); + const cancellationService: any = { + cancelUpdateJob: emptyAsyncFunc, + registerCancelableUpdateJob: emptyAsyncFunc, + }; + cancellationService.cancelUpdateJob = cancelUpdateJobSpy; + cancellationService.registerCancelableUpdateJob = registerCancelableUpdateJobSpy; + + // Setup DiskWatermarkService + const isLowWatermarkSpy = sinon.stub().resolves(true); + const diskWatermarkService: any = { + isLowWatermark: isLowWatermarkSpy, + }; + + const updateWorker = new UpdateWorker( + esQueue as Esqueue, + log, + esClient as EsClient, + { + security: { + enableGitCertCheck: true, + }, + disk: { + thresholdEnabled: true, + watermarkLowMb: 100, + }, + } as ServerOptions, + {} as GitOperations, + {} as RepositoryServiceFactory, + cancellationService as CancellationSerivce, + diskWatermarkService as DiskWatermarkService + ); + + try { + await updateWorker.executeJob({ + payload: { + uri: 'mockrepo', + }, + options: {}, + timestamp: 0, + }); + // This step should not be touched. + expect(false).toBeTruthy(); + } catch (error) { + // Exception should be thrown. + expect(isLowWatermarkSpy.calledOnce).toBeTruthy(); + expect(newInstanceSpy.notCalled).toBeTruthy(); + expect(updateSpy.notCalled).toBeTruthy(); + } +}); + +test('On update job error or timeout will not persis error', async () => { + // Setup EsClient + const esUpdateSpy = sinon.spy(); + esClient.update = esUpdateSpy; + + // Setup CancellationService + const cancelUpdateJobSpy = sinon.spy(); + const registerCancelableUpdateJobSpy = sinon.spy(); + const cancellationService: any = { + cancelUpdateJob: emptyAsyncFunc, + registerCancelableUpdateJob: emptyAsyncFunc, + }; + cancellationService.cancelUpdateJob = cancelUpdateJobSpy; + cancellationService.registerCancelableUpdateJob = registerCancelableUpdateJobSpy; + + const updateWorker = new UpdateWorker( + esQueue as Esqueue, + log, + esClient as EsClient, + { + security: { + enableGitCertCheck: true, + }, + disk: { + thresholdEnabled: true, + watermarkLowMb: 100, + }, + } as ServerOptions, + {} as GitOperations, + {} as RepositoryServiceFactory, + cancellationService as CancellationSerivce, + {} as DiskWatermarkService + ); + + await updateWorker.onJobExecutionError({ + job: { + payload: { + uri: 'mockrepo', + }, + options: {}, + timestamp: 0, + }, + error: 'mock error message', + }); + + // The elasticsearch update will be called and the progress should be 'Completed' + expect(esUpdateSpy.calledOnce).toBeTruthy(); + const updateBody = JSON.parse(esUpdateSpy.getCall(0).args[0].body); + expect(updateBody.doc.repository_git_status.progress).toBe(100); +}); diff --git a/x-pack/legacy/plugins/code/server/queue/update_worker.ts b/x-pack/legacy/plugins/code/server/queue/update_worker.ts index 716781fcb5e9..60411c4dcae6 100644 --- a/x-pack/legacy/plugins/code/server/queue/update_worker.ts +++ b/x-pack/legacy/plugins/code/server/queue/update_worker.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CloneWorkerResult, Repository } from '../../model'; +import { CloneWorkerResult, Repository, WorkerReservedProgress } from '../../model'; import { EsClient, Esqueue } from '../lib/esqueue'; +import { DiskWatermarkService } from '../disk_watermark'; import { GitOperations } from '../git_operations'; import { Logger } from '../log'; import { RepositoryServiceFactory } from '../repository_service_factory'; @@ -18,18 +19,21 @@ export class UpdateWorker extends AbstractGitWorker { public id: string = 'update'; constructor( - queue: Esqueue, + protected readonly queue: Esqueue, protected readonly log: Logger, protected readonly client: EsClient, protected readonly serverOptions: ServerOptions, protected readonly gitOps: GitOperations, protected readonly repoServiceFactory: RepositoryServiceFactory, - private readonly cancellationService: CancellationSerivce + private readonly cancellationService: CancellationSerivce, + protected readonly watermarkService: DiskWatermarkService ) { - super(queue, log, client, serverOptions, gitOps); + super(queue, log, client, serverOptions, gitOps, watermarkService); } public async executeJob(job: Job) { + await super.executeJob(job); + const { payload, cancellationToken } = job; const repo: Repository = payload; this.log.info(`Execute update job for ${repo.uri}`); @@ -73,4 +77,19 @@ export class UpdateWorker extends AbstractGitWorker { this.log.info(`Update job done for ${job.payload.uri}`); return await super.onJobCompleted(job, res); } + + public async onJobExecutionError(res: any) { + return await this.overrideUpdateErrorProgress(res); + } + + public async onJobTimeOut(res: any) { + return await this.overrideUpdateErrorProgress(res); + } + + private async overrideUpdateErrorProgress(res: any) { + this.log.warn(`Update job error`); + this.log.warn(res.error); + // Do not persist update errors assuming the next update trial is scheduling soon. + return await this.updateProgress(res.job, WorkerReservedProgress.COMPLETED); + } } diff --git a/x-pack/legacy/plugins/code/server/routes/check.ts b/x-pack/legacy/plugins/code/server/routes/check.ts index 0fcce31f2239..ad89d6281b4f 100644 --- a/x-pack/legacy/plugins/code/server/routes/check.ts +++ b/x-pack/legacy/plugins/code/server/routes/check.ts @@ -4,21 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import fetch from 'node-fetch'; + import { Logger } from '../log'; +import { ServerFacade } from '../..'; export async function checkCodeNode(url: string, log: Logger, rndStr: string) { - const res = await fetch(`${url}/api/code/codeNode?rndStr=${rndStr}`, {}); - if (res.ok) { - return await res.json(); + try { + const res = await fetch(`${url}/api/code/codeNode?rndStr=${rndStr}`, {}); + if (res.ok) { + return await res.json(); + } + } catch (e) { + // request failed + log.error(e); } log.info(`Access code node ${url} failed, try again later.`); return null; } -export function checkRoute(server: Server, rndStr: string) { +export function checkRoute(server: ServerFacade, rndStr: string) { server.route({ method: 'GET', path: '/api/code/codeNode', diff --git a/x-pack/legacy/plugins/code/server/routes/file.ts b/x-pack/legacy/plugins/code/server/routes/file.ts index 981bb71c3541..15f50e6aa9d6 100644 --- a/x-pack/legacy/plugins/code/server/routes/file.ts +++ b/x-pack/legacy/plugins/code/server/routes/file.ts @@ -4,22 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Commit, Oid, Revwalk } from '@elastic/nodegit'; import Boom from 'boom'; -import fileType from 'file-type'; -import hapi, { RequestQuery } from 'hapi'; -import { commitInfo, DEFAULT_TREE_CHILDREN_LIMIT, GitOperations } from '../git_operations'; -import { extractLines } from '../utils/buffer'; -import { detectLanguage } from '../utils/detect_language'; + +import { RequestFacade, RequestQueryFacade, ResponseToolkitFacade } from '../../'; +import { DEFAULT_TREE_CHILDREN_LIMIT } from '../git_operations'; import { CodeServerRouter } from '../security'; import { RepositoryObjectClient } from '../search'; import { EsClientWithRequest } from '../utils/esclient_with_request'; -import { TEXT_FILE_LIMIT } from '../../common/file'; import { decodeRevisionString } from '../../common/uri_util'; +import { CodeServices } from '../distributed/code_services'; +import { GitServiceDefinition } from '../distributed/apis'; + +export function fileRoute(router: CodeServerRouter, codeServices: CodeServices) { + const gitService = codeServices.serviceFor(GitServiceDefinition); -export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { async function getRepoUriFromMeta( - req: hapi.Request, + req: RequestFacade, repoUri: string ): Promise { const repoObjectClient = new RepositoryObjectClient(new EsClientWithRequest(req)); @@ -32,13 +32,13 @@ export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { } } - server.route({ + router.route({ path: '/api/code/repo/{uri*3}/tree/{ref}/{path*}', method: 'GET', - async handler(req: hapi.Request) { + async handler(req: RequestFacade) { const { uri, path, ref } = req.params; const revision = decodeRevisionString(ref); - const queries = req.query as RequestQuery; + const queries = req.query as RequestQueryFacade; const limit = queries.limit ? parseInt(queries.limit as string, 10) : DEFAULT_TREE_CHILDREN_LIMIT; @@ -49,9 +49,17 @@ export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { if (!repoUri) { return Boom.notFound(`repo ${uri} not found`); } - + const endpoint = await codeServices.locate(req, uri); try { - return await gitOps.fileTree(repoUri, path, revision, skip, limit, withParents, flatten); + return await gitService.fileTree(endpoint, { + uri: repoUri, + path, + revision, + skip, + limit, + withParents, + flatten, + }); } catch (e) { if (e.isBoom) { return e; @@ -62,52 +70,40 @@ export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { }, }); - server.route({ + router.route({ path: '/api/code/repo/{uri*3}/blob/{ref}/{path*}', method: 'GET', - async handler(req: hapi.Request, h: hapi.ResponseToolkit) { + async handler(req: RequestFacade, h: ResponseToolkitFacade) { const { uri, path, ref } = req.params; const revision = decodeRevisionString(ref); const repoUri = await getRepoUriFromMeta(req, uri); if (!repoUri) { return Boom.notFound(`repo ${uri} not found`); } + const endpoint = await codeServices.locate(req, uri); try { - const blob = await gitOps.fileContent(repoUri, path, decodeURIComponent(revision)); - if (blob.isBinary()) { - const type = fileType(blob.content()); - if (type && type.mime && type.mime.startsWith('image/')) { - const response = h.response(blob.content()); - response.type(type.mime); - return response; - } else { - // this api will return a empty response with http code 204 - return h - .response('') - .type('application/octet-stream') - .code(204); - } + const blob = await gitService.blob(endpoint, { + uri, + path, + line: (req.query as RequestQueryFacade).line as string, + revision: decodeURIComponent(revision), + }); + + if (blob.imageType) { + const response = h.response(blob.content); + response.type(blob.imageType); + return response; + } else if (blob.isBinary) { + return h + .response('') + .type('application/octet-stream') + .code(204); } else { - const line = (req.query as RequestQuery).line as string; - if (line) { - const [from, to] = line.split(','); - let fromLine = parseInt(from, 10); - let toLine = to === undefined ? fromLine + 1 : parseInt(to, 10); - if (fromLine > toLine) { - [fromLine, toLine] = [toLine, fromLine]; - } - const lines = extractLines(blob.content(), fromLine, toLine); - const lang = await detectLanguage(path, lines); + if (blob.content) { return h - .response(lines) - .type(`text/plain`) - .header('lang', lang); - } else if (blob.content().length <= TEXT_FILE_LIMIT) { - const lang = await detectLanguage(path, blob.content()); - return h - .response(blob.content()) - .type(`text/plain'`) - .header('lang', lang); + .response(blob.content) + .type('text/plain') + .header('lang', blob.lang!); } else { return h.response('').type(`text/big`); } @@ -122,22 +118,24 @@ export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { }, }); - server.route({ + router.route({ path: '/app/code/repo/{uri*3}/raw/{ref}/{path*}', method: 'GET', - async handler(req, h: hapi.ResponseToolkit) { + async handler(req: RequestFacade, h: ResponseToolkitFacade) { const { uri, path, ref } = req.params; const revision = decodeRevisionString(ref); const repoUri = await getRepoUriFromMeta(req, uri); if (!repoUri) { return Boom.notFound(`repo ${uri} not found`); } + const endpoint = await codeServices.locate(req, uri); + try { - const blob = await gitOps.fileContent(repoUri, path, revision); - if (blob.isBinary()) { - return h.response(blob.content()).type('application/octet-stream'); + const blob = await gitService.raw(endpoint, { uri: repoUri, path, revision }); + if (blob.isBinary) { + return h.response(blob.content).type('application/octet-stream'); } else { - return h.response(blob.content()).type('text/plain'); + return h.response(blob.content).type('text/plain'); } } catch (e) { if (e.isBoom) { @@ -149,22 +147,22 @@ export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { }, }); - server.route({ + router.route({ path: '/api/code/repo/{uri*3}/history/{ref}', method: 'GET', handler: historyHandler, }); - server.route({ + router.route({ path: '/api/code/repo/{uri*3}/history/{ref}/{path*}', method: 'GET', handler: historyHandler, }); - async function historyHandler(req: hapi.Request) { + async function historyHandler(req: RequestFacade) { const { uri, ref, path } = req.params; const revision = decodeRevisionString(ref); - const queries = req.query as RequestQuery; + const queries = req.query as RequestQueryFacade; const count = queries.count ? parseInt(queries.count as string, 10) : 10; const after = queries.after !== undefined; try { @@ -172,29 +170,8 @@ export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { if (!repoUri) { return Boom.notFound(`repo ${uri} not found`); } - const repository = await gitOps.openRepo(repoUri); - const commit = await gitOps.getCommitInfo(repoUri, revision); - if (commit === null) { - throw Boom.notFound(`commit ${revision} not found in repo ${uri}`); - } - const walk = repository.createRevWalk(); - walk.sorting(Revwalk.SORT.TIME); - const commitId = Oid.fromString(commit!.id); - walk.push(commitId); - let commits: Commit[]; - if (path) { - // magic number 10000: how many commits at the most to iterate in order to find the commits contains the path - const results = await walk.fileHistoryWalk(path, count, 10000); - commits = results.map(result => result.commit); - } else { - commits = await walk.getCommits(count); - } - if (after && commits.length > 0) { - if (commits[0].id().equal(commitId)) { - commits = commits.slice(1); - } - } - return commits.map(commitInfo); + const endpoint = await codeServices.locate(req, uri); + return await gitService.history(endpoint, { uri: repoUri, path, revision, count, after }); } catch (e) { if (e.isBoom) { return e; @@ -203,17 +180,20 @@ export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { } } } - server.route({ + + router.route({ path: '/api/code/repo/{uri*3}/references', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { const uri = req.params.uri; const repoUri = await getRepoUriFromMeta(req, uri); if (!repoUri) { return Boom.notFound(`repo ${uri} not found`); } + const endpoint = await codeServices.locate(req, uri); + try { - return await gitOps.getBranchAndTags(repoUri); + return await gitService.branchesAndTags(endpoint, { uri: repoUri }); } catch (e) { if (e.isBoom) { return e; @@ -224,18 +204,21 @@ export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { }, }); - server.route({ + router.route({ path: '/api/code/repo/{uri*3}/diff/{revision}', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { const { uri, revision } = req.params; const repoUri = await getRepoUriFromMeta(req, uri); if (!repoUri) { return Boom.notFound(`repo ${uri} not found`); } + const endpoint = await codeServices.locate(req, uri); try { - const diff = await gitOps.getCommitDiff(repoUri, decodeRevisionString(revision)); - return diff; + return await gitService.commitDiff(endpoint, { + uri: repoUri, + revision: decodeRevisionString(revision), + }); } catch (e) { if (e.isBoom) { return e; @@ -246,22 +229,23 @@ export function fileRoute(server: CodeServerRouter, gitOps: GitOperations) { }, }); - server.route({ + router.route({ path: '/api/code/repo/{uri*3}/blame/{revision}/{path*}', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { const { uri, path, revision } = req.params; const repoUri = await getRepoUriFromMeta(req, uri); if (!repoUri) { return Boom.notFound(`repo ${uri} not found`); } + const endpoint = await codeServices.locate(req, uri); + try { - const blames = await gitOps.blame( - repoUri, - decodeRevisionString(decodeURIComponent(revision)), - path - ); - return blames; + return await gitService.blame(endpoint, { + uri: repoUri, + revision: decodeRevisionString(decodeURIComponent(revision)), + path, + }); } catch (e) { if (e.isBoom) { return e; diff --git a/x-pack/legacy/plugins/code/server/routes/index.ts b/x-pack/legacy/plugins/code/server/routes/index.ts new file mode 100644 index 000000000000..27f40de552a3 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/routes/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. + */ + +export * from './check'; +export * from './file'; +export * from './install'; +export * from './lsp'; +export * from './redirect'; +export * from './repository'; +export * from './search'; +export * from './setup'; +export * from './status'; +export * from './workspace'; diff --git a/x-pack/legacy/plugins/code/server/routes/install.ts b/x-pack/legacy/plugins/code/server/routes/install.ts index 78a7ce9b1062..fb1ea08849b5 100644 --- a/x-pack/legacy/plugins/code/server/routes/install.ts +++ b/x-pack/legacy/plugins/code/server/routes/install.ts @@ -5,16 +5,20 @@ */ import * as Boom from 'boom'; -import { Request } from 'hapi'; + +import { RequestFacade } from '../..'; import { enabledLanguageServers, LanguageServerDefinition } from '../lsp/language_servers'; -import { LspService } from '../lsp/lsp_service'; import { CodeServerRouter } from '../security'; +import { CodeServices } from '../distributed/code_services'; +import { LspServiceDefinition } from '../distributed/apis'; +import { Endpoint } from '../distributed/resource_locator'; -export function installRoute(server: CodeServerRouter, lspService: LspService) { - const kibanaVersion = server.server.config().get('pkg.version') as string; - const status = (def: LanguageServerDefinition) => ({ +export function installRoute(router: CodeServerRouter, codeServices: CodeServices) { + const lspService = codeServices.serviceFor(LspServiceDefinition); + const kibanaVersion = router.server.config().get('pkg.version') as string; + const status = (endpoint: Endpoint, def: LanguageServerDefinition) => ({ name: def.name, - status: lspService.languageServerStatus(def.name), + status: lspService.languageServerStatus(endpoint, { langName: def.name }), version: def.version, build: def.build, languages: def.languages, @@ -24,21 +28,23 @@ export function installRoute(server: CodeServerRouter, lspService: LspService) { pluginName: def.installationPluginName, }); - server.route({ + router.route({ path: '/api/code/install', - handler() { - return enabledLanguageServers(server.server).map(status); + async handler(req: RequestFacade) { + const endpoint = await codeServices.locate(req, ''); + return enabledLanguageServers(router.server).map(def => status(endpoint, def)); }, method: 'GET', }); - server.route({ + router.route({ path: '/api/code/install/{name}', - handler(req: Request) { + async handler(req: RequestFacade) { const name = req.params.name; - const def = enabledLanguageServers(server.server).find(d => d.name === name); + const def = enabledLanguageServers(router.server).find(d => d.name === name); + const endpoint = await codeServices.locate(req, ''); if (def) { - return status(def); + return status(endpoint, def); } else { return Boom.notFound(`language server ${name} not found.`); } diff --git a/x-pack/legacy/plugins/code/server/routes/lsp.ts b/x-pack/legacy/plugins/code/server/routes/lsp.ts index 965b0b9c69c6..edeefe8f9808 100644 --- a/x-pack/legacy/plugins/code/server/routes/lsp.ts +++ b/x-pack/legacy/plugins/code/server/routes/lsp.ts @@ -5,21 +5,19 @@ */ import Boom from 'boom'; -import hapi from 'hapi'; import { groupBy, last } from 'lodash'; import { ResponseError } from 'vscode-jsonrpc'; import { ResponseMessage } from 'vscode-jsonrpc/lib/messages'; import { Location } from 'vscode-languageserver-types'; + import { LanguageServerStartFailed, ServerNotInitialized, UnknownFileLanguage, } from '../../common/lsp_error_codes'; import { parseLspUrl } from '../../common/uri_util'; -import { GitOperations } from '../git_operations'; import { Logger } from '../log'; import { CTAGS, GO } from '../lsp/language_servers'; -import { LspService } from '../lsp/lsp_service'; import { SymbolSearchClient } from '../search'; import { CodeServerRouter } from '../security'; import { ServerOptions } from '../server_options'; @@ -32,28 +30,37 @@ import { import { detectLanguage } from '../utils/detect_language'; import { EsClientWithRequest } from '../utils/esclient_with_request'; import { promiseTimeout } from '../utils/timeout'; +import { RequestFacade, ResponseToolkitFacade } from '../..'; +import { CodeServices } from '../distributed/code_services'; +import { GitServiceDefinition, LspServiceDefinition } from '../distributed/apis'; const LANG_SERVER_ERROR = 'language server error'; export function lspRoute( server: CodeServerRouter, - lspService: LspService, + codeServices: CodeServices, serverOptions: ServerOptions ) { const log = new Logger(server.server); - + const lspService = codeServices.serviceFor(LspServiceDefinition); + const gitService = codeServices.serviceFor(GitServiceDefinition); server.route({ path: '/api/code/lsp/textDocument/{method}', - async handler(req, h: hapi.ResponseToolkit) { + async handler(req: RequestFacade, h: ResponseToolkitFacade) { if (typeof req.payload === 'object' && req.payload != null) { const method = req.params.method; if (method) { try { - const result = await promiseTimeout( - serverOptions.lsp.requestTimeoutMs, - lspService.sendRequest(`textDocument/${method}`, req.payload, 1000) - ); - return result; + const params = (req.payload as unknown) as any; + const uri = params.textDocument.uri; + const { repoUri } = parseLspUrl(uri)!; + const endpoint = await codeServices.locate(req, repoUri); + const requestPromise = lspService.sendRequest(endpoint, { + method: `textDocument/${method}`, + params: req.payload, + timeoutForInitializeMs: 1000, + }); + return await promiseTimeout(serverOptions.lsp.requestTimeoutMs, requestPromise); } catch (error) { if (error instanceof ResponseError) { // hide some errors; @@ -91,22 +98,26 @@ export function lspRoute( server.route({ path: '/api/code/lsp/findReferences', method: 'POST', - async handler(req, h: hapi.ResponseToolkit) { + async handler(req: RequestFacade, h: ResponseToolkitFacade) { try { // @ts-ignore const { textDocument, position } = req.payload; const { uri } = textDocument; + const endpoint = await codeServices.locate(req, parseLspUrl(uri).repoUri); const response: ResponseMessage = await promiseTimeout( serverOptions.lsp.requestTimeoutMs, - lspService.sendRequest( - `textDocument/references`, - { textDocument: { uri }, position }, - 1000 - ) + lspService.sendRequest(endpoint, { + method: `textDocument/references`, + params: { textDocument: { uri }, position }, + timeoutForInitializeMs: 1000, + }) ); - const hover = await lspService.sendRequest('textDocument/hover', { - textDocument: { uri }, - position, + const hover = await lspService.sendRequest(endpoint, { + method: 'textDocument/hover', + params: { + textDocument: { uri }, + position, + }, }); let title: string; if (hover.result && hover.result.contents) { @@ -136,11 +147,11 @@ export function lspRoute( } else { title = last(uri.toString().split('/')) + `(${position.line}, ${position.character})`; } - const gitOperations = new GitOperations(serverOptions.repoPath); const files = []; const groupedLocations = groupBy(response.result as Location[], 'uri'); for (const url of Object.keys(groupedLocations)) { const { repoUri, revision, file } = parseLspUrl(url)!; + const ep = await codeServices.locate(req, repoUri); const locations: Location[] = groupedLocations[url]; const lines = locations.map(l => ({ startLine: l.range.start.line, @@ -148,36 +159,35 @@ export function lspRoute( })); const ranges = expandRanges(lines, 1); const mergedRanges = mergeRanges(ranges); - const blob = await gitOperations.fileContent(repoUri, file!, revision); - const source = blob - .content() - .toString('utf8') - .split('\n'); - const language = await detectLanguage(file!, blob.content()); - const lineMappings = new LineMapping(); - const code = extractSourceContent(mergedRanges, source, lineMappings).join('\n'); - const lineNumbers = lineMappings.toStringArray(); - const highlights = locations.map(l => { - const { start, end } = l.range; - const startLineNumber = lineMappings.lineNumber(start.line); - const endLineNumber = lineMappings.lineNumber(end.line); - return { - startLineNumber, - startColumn: start.character + 1, - endLineNumber, - endColumn: end.character + 1, - }; - }); - files.push({ - repo: repoUri, - file, - language, - uri: url, - revision, - code, - lineNumbers, - highlights, - }); + const blob = await gitService.blob(ep, { uri: repoUri, path: file!, revision }); + if (blob.content) { + const source = blob.content.split('\n'); + const language = blob.lang; + const lineMappings = new LineMapping(); + const code = extractSourceContent(mergedRanges, source, lineMappings).join('\n'); + const lineNumbers = lineMappings.toStringArray(); + const highlights = locations.map(l => { + const { start, end } = l.range; + const startLineNumber = lineMappings.lineNumber(start.line); + const endLineNumber = lineMappings.lineNumber(end.line); + return { + startLineNumber, + startColumn: start.character + 1, + endLineNumber, + endColumn: end.character + 1, + }; + }); + files.push({ + repo: repoUri, + file, + language, + uri: url, + revision, + code, + lineNumbers, + highlights, + }); + } } return { title, files: groupBy(files, 'repo'), uri, position }; } catch (error) { @@ -200,11 +210,11 @@ export function lspRoute( }); } -export function symbolByQnameRoute(server: CodeServerRouter, log: Logger) { - server.route({ +export function symbolByQnameRoute(router: CodeServerRouter, log: Logger) { + router.route({ path: '/api/code/lsp/symbol/{qname}', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { try { const symbolSearchClient = new SymbolSearchClient(new EsClientWithRequest(req), log); const res = await symbolSearchClient.findByQname(req.params.qname); diff --git a/x-pack/legacy/plugins/code/server/routes/redirect.ts b/x-pack/legacy/plugins/code/server/routes/redirect.ts index 17084a98f738..2882a3733483 100644 --- a/x-pack/legacy/plugins/code/server/routes/redirect.ts +++ b/x-pack/legacy/plugins/code/server/routes/redirect.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import hapi from 'hapi'; +import { RequestFacade, ServerFacade } from '../../'; import { Logger } from '../log'; -export function redirectRoute(server: hapi.Server, redirectUrl: string, log: Logger) { +export function redirectRoute(server: ServerFacade, redirectUrl: string, log: Logger) { const proxyHandler = { proxy: { passThrough: true, - async mapUri(request: hapi.Request) { + async mapUri(request: RequestFacade) { let uri; uri = `${redirectUrl}${request.path}`; if (request.url.search) { diff --git a/x-pack/legacy/plugins/code/server/routes/repository.ts b/x-pack/legacy/plugins/code/server/routes/repository.ts index 22afc40de1c2..88e4d1f10b90 100644 --- a/x-pack/legacy/plugins/code/server/routes/repository.ts +++ b/x-pack/legacy/plugins/code/server/routes/repository.ts @@ -6,33 +6,34 @@ import Boom from 'boom'; +import { RequestFacade, ResponseToolkitFacade } from '../..'; import { validateGitUrl } from '../../common/git_url_utils'; import { RepositoryUtils } from '../../common/repository_utils'; -import { RepositoryConfig, RepositoryUri } from '../../model'; +import { RepositoryConfig, RepositoryUri, WorkerReservedProgress } from '../../model'; import { RepositoryIndexInitializer, RepositoryIndexInitializerFactory } from '../indexer'; import { Logger } from '../log'; -import { CloneWorker, DeleteWorker, IndexWorker } from '../queue'; import { RepositoryConfigController } from '../repository_config_controller'; import { RepositoryObjectClient } from '../search'; import { ServerOptions } from '../server_options'; import { EsClientWithRequest } from '../utils/esclient_with_request'; import { CodeServerRouter } from '../security'; +import { CodeServices } from '../distributed/code_services'; +import { RepositoryServiceDefinition } from '../distributed/apis'; export function repositoryRoute( - server: CodeServerRouter, - cloneWorker: CloneWorker, - deleteWorker: DeleteWorker, - indexWorker: IndexWorker, + router: CodeServerRouter, + codeServices: CodeServices, repoIndexInitializerFactory: RepositoryIndexInitializerFactory, repoConfigController: RepositoryConfigController, options: ServerOptions ) { + const repositoryService = codeServices.serviceFor(RepositoryServiceDefinition); // Clone a git repository - server.route({ + router.route({ path: '/api/code/repo', requireAdmin: true, method: 'POST', - async handler(req, h) { + async handler(req: RequestFacade, h: ResponseToolkitFacade) { const repoUrl: string = (req.payload as any).url; const log = new Logger(req.server); @@ -78,7 +79,8 @@ export function repositoryRoute( const payload = { url: repoUrl, }; - await cloneWorker.enqueueJob(payload, {}); + const endpoint = await codeServices.locate(req, repoUrl); + await repositoryService.clone(endpoint, payload); return repo; } catch (error2) { const msg = `Issue repository clone request for ${repoUrl} error`; @@ -91,11 +93,11 @@ export function repositoryRoute( }); // Remove a git repository - server.route({ + router.route({ path: '/api/code/repo/{uri*3}', requireAdmin: true, method: 'DELETE', - async handler(req, h) { + async handler(req: RequestFacade, h: ResponseToolkitFacade) { const repoUri: string = req.params.uri as string; const log = new Logger(req.server); const repoObjectClient = new RepositoryObjectClient(new EsClientWithRequest(req)); @@ -106,10 +108,13 @@ export function repositoryRoute( // Check if the repository delete status already exists. If so, we should ignore this // request. try { - await repoObjectClient.getRepositoryDeleteStatus(repoUri); - const msg = `Repository ${repoUri} is already in delete.`; - log.info(msg); - return h.response(msg).code(304); // Not Modified + const status = await repoObjectClient.getRepositoryDeleteStatus(repoUri); + // if the delete status is an ERROR, we can give it another try + if (status.progress !== WorkerReservedProgress.ERROR) { + const msg = `Repository ${repoUri} is already in delete.`; + log.info(msg); + return h.response(msg).code(304); // Not Modified + } } catch (error) { // Do nothing here since this error is expected. log.info(`Repository ${repoUri} delete status does not exist. Go ahead with delete.`); @@ -118,8 +123,8 @@ export function repositoryRoute( const payload = { uri: repoUri, }; - await deleteWorker.enqueueJob(payload, {}); - + const endpoint = await codeServices.locate(req, repoUri); + await repositoryService.delete(endpoint, payload); return {}; } catch (error) { const msg = `Issue repository delete request for ${repoUri} error`; @@ -131,10 +136,10 @@ export function repositoryRoute( }); // Get a git repository - server.route({ + router.route({ path: '/api/code/repo/{uri*3}', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { const repoUri = req.params.uri as string; const log = new Logger(req.server); try { @@ -149,10 +154,10 @@ export function repositoryRoute( }, }); - server.route({ + router.route({ path: '/api/code/repo/status/{uri*3}', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { const repoUri = req.params.uri as string; const log = new Logger(req.server); try { @@ -192,10 +197,10 @@ export function repositoryRoute( }); // Get all git repositories - server.route({ + router.route({ path: '/api/code/repos', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { const log = new Logger(req.server); try { const repoObjectClient = new RepositoryObjectClient(new EsClientWithRequest(req)); @@ -212,11 +217,11 @@ export function repositoryRoute( // Issue a repository index task. // TODO(mengwei): This is just temporary API stub to trigger the index job. Eventually in the near // future, this route will be removed. The scheduling strategy is still in discussion. - server.route({ + router.route({ path: '/api/code/repo/index/{uri*3}', method: 'POST', requireAdmin: true, - async handler(req) { + async handler(req: RequestFacade) { const repoUri = req.params.uri as string; const log = new Logger(req.server); const reindex: boolean = (req.payload as any).reindex; @@ -229,7 +234,8 @@ export function repositoryRoute( revision: cloneStatus.revision, enforceReindex: reindex, }; - await indexWorker.enqueueJob(payload, {}); + const endpoint = await codeServices.locate(req, repoUri); + await repositoryService.index(endpoint, payload); return {}; } catch (error) { const msg = `Index repository ${repoUri} error`; @@ -241,11 +247,11 @@ export function repositoryRoute( }); // Update a repo config - server.route({ + router.route({ path: '/api/code/repo/config/{uri*3}', method: 'PUT', requireAdmin: true, - async handler(req, h) { + async handler(req: RequestFacade) { const config: RepositoryConfig = req.payload as RepositoryConfig; const repoUri: RepositoryUri = config.uri; const log = new Logger(req.server); @@ -273,10 +279,10 @@ export function repositoryRoute( }); // Get repository config - server.route({ + router.route({ path: '/api/code/repo/config/{uri*3}', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { const repoUri = req.params.uri as string; try { const repoObjectClient = new RepositoryObjectClient(new EsClientWithRequest(req)); diff --git a/x-pack/legacy/plugins/code/server/routes/search.ts b/x-pack/legacy/plugins/code/server/routes/search.ts index 8b759f502ddf..f2a0ba4db705 100644 --- a/x-pack/legacy/plugins/code/server/routes/search.ts +++ b/x-pack/legacy/plugins/code/server/routes/search.ts @@ -6,20 +6,20 @@ import Boom from 'boom'; -import hapi from 'hapi'; +import { RequestFacade, RequestQueryFacade } from '../../'; import { DocumentSearchRequest, RepositorySearchRequest, SymbolSearchRequest } from '../../model'; import { Logger } from '../log'; import { DocumentSearchClient, RepositorySearchClient, SymbolSearchClient } from '../search'; import { EsClientWithRequest } from '../utils/esclient_with_request'; import { CodeServerRouter } from '../security'; -export function repositorySearchRoute(server: CodeServerRouter, log: Logger) { - server.route({ +export function repositorySearchRoute(router: CodeServerRouter, log: Logger) { + router.route({ path: '/api/code/search/repo', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { let page = 1; - const { p, q, repoScope } = req.query as hapi.RequestQuery; + const { p, q, repoScope } = req.query as RequestQueryFacade; if (p) { page = parseInt(p as string, 10); } @@ -44,12 +44,12 @@ export function repositorySearchRoute(server: CodeServerRouter, log: Logger) { }, }); - server.route({ + router.route({ path: '/api/code/suggestions/repo', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { let page = 1; - const { p, q, repoScope } = req.query as hapi.RequestQuery; + const { p, q, repoScope } = req.query as RequestQueryFacade; if (p) { page = parseInt(p as string, 10); } @@ -75,13 +75,13 @@ export function repositorySearchRoute(server: CodeServerRouter, log: Logger) { }); } -export function documentSearchRoute(server: CodeServerRouter, log: Logger) { - server.route({ +export function documentSearchRoute(router: CodeServerRouter, log: Logger) { + router.route({ path: '/api/code/search/doc', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { let page = 1; - const { p, q, langs, repos, repoScope } = req.query as hapi.RequestQuery; + const { p, q, langs, repos, repoScope } = req.query as RequestQueryFacade; if (p) { page = parseInt(p as string, 10); } @@ -108,12 +108,12 @@ export function documentSearchRoute(server: CodeServerRouter, log: Logger) { }, }); - server.route({ + router.route({ path: '/api/code/suggestions/doc', method: 'GET', - async handler(req) { + async handler(req: RequestFacade) { let page = 1; - const { p, q, repoScope } = req.query as hapi.RequestQuery; + const { p, q, repoScope } = req.query as RequestQueryFacade; if (p) { page = parseInt(p as string, 10); } @@ -139,10 +139,10 @@ export function documentSearchRoute(server: CodeServerRouter, log: Logger) { }); } -export function symbolSearchRoute(server: CodeServerRouter, log: Logger) { - const symbolSearchHandler = async (req: hapi.Request) => { +export function symbolSearchRoute(router: CodeServerRouter, log: Logger) { + const symbolSearchHandler = async (req: RequestFacade) => { let page = 1; - const { p, q, repoScope } = req.query as hapi.RequestQuery; + const { p, q, repoScope } = req.query as RequestQueryFacade; if (p) { page = parseInt(p as string, 10); } @@ -167,12 +167,12 @@ export function symbolSearchRoute(server: CodeServerRouter, log: Logger) { }; // Currently these 2 are the same. - server.route({ + router.route({ path: '/api/code/suggestions/symbol', method: 'GET', handler: symbolSearchHandler, }); - server.route({ + router.route({ path: '/api/code/search/symbol', method: 'GET', handler: symbolSearchHandler, diff --git a/x-pack/legacy/plugins/code/server/routes/setup.ts b/x-pack/legacy/plugins/code/server/routes/setup.ts index 0c75b8ec1d46..58db84fd80aa 100644 --- a/x-pack/legacy/plugins/code/server/routes/setup.ts +++ b/x-pack/legacy/plugins/code/server/routes/setup.ts @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResponseToolkit } from 'hapi'; +import { RequestFacade } from '../..'; import { CodeServerRouter } from '../security'; +import { CodeServices } from '../distributed/code_services'; +import { SetupDefinition } from '../distributed/apis'; -export function setupRoute(server: CodeServerRouter) { - server.route({ +export function setupRoute(router: CodeServerRouter, codeServices: CodeServices) { + const setupService = codeServices.serviceFor(SetupDefinition); + router.route({ method: 'get', path: '/api/code/setup', - handler(req, h: ResponseToolkit) { - return h.response('').code(200); + async handler(req: RequestFacade) { + const endpoint = await codeServices.locate(req, ''); + return await setupService.setup(endpoint, {}); }, }); } diff --git a/x-pack/legacy/plugins/code/server/routes/status.ts b/x-pack/legacy/plugins/code/server/routes/status.ts index 6589b73ee56e..7a4a062926cf 100644 --- a/x-pack/legacy/plugins/code/server/routes/status.ts +++ b/x-pack/legacy/plugins/code/server/routes/status.ts @@ -3,35 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import hapi from 'hapi'; + import Boom from 'boom'; -import { LspService } from '../lsp/lsp_service'; -import { GitOperations } from '../git_operations'; import { CodeServerRouter } from '../security'; +import { RequestFacade } from '../../'; import { LangServerType, RepoFileStatus, StatusReport } from '../../common/repo_file_status'; import { CTAGS, LanguageServerDefinition } from '../lsp/language_servers'; import { LanguageServerStatus } from '../../common/language_server'; import { WorkspaceStatus } from '../lsp/request_expander'; import { RepositoryObjectClient } from '../search'; import { EsClientWithRequest } from '../utils/esclient_with_request'; -import { TEXT_FILE_LIMIT } from '../../common/file'; -import { detectLanguage } from '../utils/detect_language'; +import { CodeServices } from '../distributed/code_services'; +import { GitServiceDefinition, LspServiceDefinition } from '../distributed/apis'; +import { Endpoint } from '../distributed/resource_locator'; -export function statusRoute( - server: CodeServerRouter, - gitOps: GitOperations, - lspService: LspService -) { +export function statusRoute(router: CodeServerRouter, codeServices: CodeServices) { + const gitService = codeServices.serviceFor(GitServiceDefinition); + const lspService = codeServices.serviceFor(LspServiceDefinition); async function handleRepoStatus( + endpoint: Endpoint, report: StatusReport, repoUri: string, revision: string, repoObjectClient: RepositoryObjectClient ) { - const commit = await gitOps.getCommit(repoUri, decodeURIComponent(revision)); - const head = await gitOps.getHeadRevision(repoUri); - if (head === commit.sha()) { + const commit = await gitService.commit(endpoint, { + uri: repoUri, + revision: decodeURIComponent(revision), + }); + const head = await gitService.headRevision(endpoint, { uri: repoUri }); + if (head === commit.id) { try { const indexStatus = await repoObjectClient.getRepositoryLspIndexStatus(repoUri); if (indexStatus.progress < 100) { @@ -46,62 +48,84 @@ export function statusRoute( } } - async function handleFileStatus(report: StatusReport, content: Buffer, path: string) { - if (content.length <= TEXT_FILE_LIMIT) { - const lang: string = await detectLanguage(path, content); - const def = lspService.getLanguageSeverDef(lang); - if (def === null) { + async function handleFileStatus( + endpoint: Endpoint, + report: StatusReport, + blob: { isBinary: boolean; imageType?: string; content?: string; lang?: string } + ) { + if (blob.content) { + const lang: string = blob.lang!; + const defs = await lspService.languageSeverDef(endpoint, { lang }); + if (defs.length === 0) { report.fileStatus = RepoFileStatus.FILE_NOT_SUPPORTED; } else { - return def; + return defs; } } else { report.fileStatus = RepoFileStatus.FILE_IS_TOO_BIG; } + return []; } async function handleLspStatus( + endpoint: Endpoint, report: StatusReport, - def: LanguageServerDefinition, + defs: LanguageServerDefinition[], repoUri: string, revision: string ) { - report.langServerType = def === CTAGS ? LangServerType.GENERIC : LangServerType.DEDICATED; - if (lspService.languageServerStatus(def.languages[0]) === LanguageServerStatus.NOT_INSTALLED) { + const dedicated = defs.find(d => d !== CTAGS); + const generic = defs.find(d => d === CTAGS); + report.langServerType = dedicated ? LangServerType.DEDICATED : LangServerType.GENERIC; + if ( + dedicated && + (await lspService.languageServerStatus(endpoint, { langName: dedicated.name })) === + LanguageServerStatus.NOT_INSTALLED + ) { report.langServerStatus = RepoFileStatus.LANG_SERVER_NOT_INSTALLED; + if (generic) { + // dedicated lang server not installed, fallback to generic + report.langServerType = LangServerType.GENERIC; + } } else { - const state = await lspService.initializeState(repoUri, revision); - const initState = state[def.name]; + const def = dedicated || generic; + const state = await lspService.initializeState(endpoint, { repoUri, revision }); + const initState = state[def!.name]; report.langServerStatus = initState === WorkspaceStatus.Initialized ? RepoFileStatus.LANG_SERVER_INITIALIZED : RepoFileStatus.LANG_SERVER_IS_INITIALIZING; } } - - server.route({ + router.route({ path: '/api/code/repo/{uri*3}/status/{ref}/{path*}', method: 'GET', - async handler(req: hapi.Request) { + async handler(req: RequestFacade) { const { uri, path, ref } = req.params; const report: StatusReport = {}; const repoObjectClient = new RepositoryObjectClient(new EsClientWithRequest(req)); + const endpoint = await codeServices.locate(req, uri); + try { // Check if the repository already exists await repoObjectClient.getRepository(uri); } catch (e) { return Boom.notFound(`repo ${uri} not found`); } - await handleRepoStatus(report, uri, ref, repoObjectClient); + await handleRepoStatus(endpoint, report, uri, ref, repoObjectClient); if (path) { try { try { - const blob = await gitOps.fileContent(uri, path, decodeURIComponent(ref)); + const blob = await gitService.blob(endpoint, { + uri, + path, + revision: decodeURIComponent(ref), + }); // text file - if (!blob.isBinary()) { - const def = await handleFileStatus(report, blob.content(), path); - if (def) { - await handleLspStatus(report, def, uri, ref); + if (!blob.isBinary) { + const defs = await handleFileStatus(endpoint, report, blob); + if (defs.length > 0) { + await handleLspStatus(endpoint, report, defs, uri, ref); } } } catch (e) { diff --git a/x-pack/legacy/plugins/code/server/routes/workspace.ts b/x-pack/legacy/plugins/code/server/routes/workspace.ts index a1e3aa78c492..708c2ebe8ac7 100644 --- a/x-pack/legacy/plugins/code/server/routes/workspace.ts +++ b/x-pack/legacy/plugins/code/server/routes/workspace.ts @@ -5,23 +5,21 @@ */ import Boom from 'boom'; -import hapi, { RequestQuery } from 'hapi'; -import { GitOperations } from '../git_operations'; -import { Logger } from '../log'; -import { WorkspaceCommand } from '../lsp/workspace_command'; -import { WorkspaceHandler } from '../lsp/workspace_handler'; +import { RequestFacade, RequestQueryFacade } from '../../'; import { ServerOptions } from '../server_options'; -import { EsClientWithRequest } from '../utils/esclient_with_request'; -import { ServerLoggerFactory } from '../utils/server_logger_factory'; import { CodeServerRouter } from '../security'; +import { CodeServices } from '../distributed/code_services'; +import { WorkspaceDefinition } from '../distributed/apis'; export function workspaceRoute( - server: CodeServerRouter, + router: CodeServerRouter, serverOptions: ServerOptions, - gitOps: GitOperations + codeServices: CodeServices ) { - server.route({ + const workspaceService = codeServices.serviceFor(WorkspaceDefinition); + + router.route({ path: '/api/code/workspace', method: 'GET', async handler() { @@ -29,37 +27,19 @@ export function workspaceRoute( }, }); - server.route({ + router.route({ path: '/api/code/workspace/{uri*3}/{revision}', requireAdmin: true, method: 'POST', - async handler(req: hapi.Request, reply) { + async handler(req: RequestFacade) { const repoUri = req.params.uri as string; const revision = req.params.revision as string; const repoConfig = serverOptions.repoConfigs[repoUri]; - const force = !!(req.query as RequestQuery).force; + const force = !!(req.query as RequestQueryFacade).force; if (repoConfig) { - const log = new Logger(server.server, ['workspace', repoUri]); - const workspaceHandler = new WorkspaceHandler( - gitOps, - serverOptions.workspacePath, - new EsClientWithRequest(req), - new ServerLoggerFactory(server.server) - ); + const endpoint = await codeServices.locate(req, repoUri); try { - const { workspaceDir, workspaceRevision } = await workspaceHandler.openWorkspace( - repoUri, - revision - ); - const workspaceCmd = new WorkspaceCommand( - repoConfig, - workspaceDir, - workspaceRevision, - log - ); - workspaceCmd.runInit(force).then(() => { - return ''; - }); + await workspaceService.initCmd(endpoint, { repoUri, revision, force, repoConfig }); } catch (e) { if (e.isBoom) { return e; diff --git a/x-pack/legacy/plugins/code/server/scheduler/index_scheduler.ts b/x-pack/legacy/plugins/code/server/scheduler/index_scheduler.ts index 8008f44e6a91..e8f8ba1928a5 100644 --- a/x-pack/legacy/plugins/code/server/scheduler/index_scheduler.ts +++ b/x-pack/legacy/plugins/code/server/scheduler/index_scheduler.ts @@ -32,7 +32,7 @@ export class IndexScheduler extends AbstractScheduler { } protected async executeSchedulingJob(repo: Repository) { - this.log.info(`Schedule index repo request for ${repo.uri}`); + this.log.debug(`Schedule index repo request for ${repo.uri}`); try { // This repository is too soon to execute the next index job. if (repo.nextIndexTimestamp && new Date() < new Date(repo.nextIndexTimestamp)) { @@ -44,7 +44,7 @@ export class IndexScheduler extends AbstractScheduler { !RepositoryUtils.hasFullyCloned(cloneStatus.cloneProgress) || cloneStatus.progress !== WorkerReservedProgress.COMPLETED ) { - this.log.info(`Repo ${repo.uri} has not been fully cloned yet or in update. Skip index.`); + this.log.debug(`Repo ${repo.uri} has not been fully cloned yet or in update. Skip index.`); return; } @@ -52,19 +52,19 @@ export class IndexScheduler extends AbstractScheduler { // Schedule index job only when the indexed revision is different from the current repository // revision. - this.log.info( + this.log.debug( `Current repo revision: ${repo.revision}, indexed revision ${repoIndexStatus.revision}.` ); if ( repoIndexStatus.progress >= 0 && repoIndexStatus.progress < WorkerReservedProgress.COMPLETED ) { - this.log.info(`Repo is still in indexing. Skip index for ${repo.uri}`); + this.log.debug(`Repo is still in indexing. Skip index for ${repo.uri}`); } else if ( repoIndexStatus.progress === WorkerReservedProgress.COMPLETED && repoIndexStatus.revision === repo.revision ) { - this.log.info(`Repo does not change since last index. Skip index for ${repo.uri}.`); + this.log.debug(`Repo does not change since last index. Skip index for ${repo.uri}.`); } else { const payload = { uri: repo.uri, diff --git a/x-pack/legacy/plugins/code/server/scheduler/update_scheduler.ts b/x-pack/legacy/plugins/code/server/scheduler/update_scheduler.ts index 1ff0f29095c6..7cc2daa0fbe6 100644 --- a/x-pack/legacy/plugins/code/server/scheduler/update_scheduler.ts +++ b/x-pack/legacy/plugins/code/server/scheduler/update_scheduler.ts @@ -41,7 +41,7 @@ export class UpdateScheduler extends AbstractScheduler { this.log.debug(`Repo ${repo.uri} is too soon to execute the next update job.`); return; } - this.log.info(`Start to schedule update repo request for ${repo.uri}`); + this.log.debug(`Start to schedule update repo request for ${repo.uri}`); let inDelete = false; try { @@ -69,7 +69,7 @@ export class UpdateScheduler extends AbstractScheduler { await this.updateWorker.enqueueJob(payload, {}); } else { - this.log.info( + this.log.debug( `Repo ${repo.uri} has not been fully cloned yet or in update/delete. Skip update.` ); } diff --git a/x-pack/legacy/plugins/code/server/security.ts b/x-pack/legacy/plugins/code/server/security.ts index 50368117904a..c548b5194059 100644 --- a/x-pack/legacy/plugins/code/server/security.ts +++ b/x-pack/legacy/plugins/code/server/security.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server, ServerRoute, RouteOptions } from 'hapi'; +import { ServerFacade, ServerRouteFacade, RouteOptionsFacade } from '..'; export class CodeServerRouter { - constructor(readonly server: Server) {} + constructor(readonly server: ServerFacade) {} route(route: CodeRoute) { - const routeOptions: RouteOptions = (route.options || {}) as RouteOptions; + const routeOptions: RouteOptionsFacade = (route.options || {}) as RouteOptionsFacade; routeOptions.tags = [ ...(routeOptions.tags || []), `access:code_${route.requireAdmin ? 'admin' : 'user'}`, @@ -25,6 +25,6 @@ export class CodeServerRouter { } } -export interface CodeRoute extends ServerRoute { +export interface CodeRoute extends ServerRouteFacade { requireAdmin?: boolean; } diff --git a/x-pack/legacy/plugins/code/server/server_options.ts b/x-pack/legacy/plugins/code/server/server_options.ts index 35525d1db8f4..3c8a143a3cf2 100644 --- a/x-pack/legacy/plugins/code/server/server_options.ts +++ b/x-pack/legacy/plugins/code/server/server_options.ts @@ -22,6 +22,11 @@ export interface SecurityOptions { enableGitCertCheck: boolean; } +export interface DiskOptions { + thresholdEnabled: boolean; + watermarkLowMb: number; +} + export class ServerOptions { public readonly workspacePath = resolve(this.config.get('path.data'), 'code/workspace'); @@ -33,6 +38,8 @@ export class ServerOptions { public readonly jdtConfigPath = resolve(this.config.get('path.data'), 'code/jdt_config'); + public readonly goPath = resolve(this.config.get('path.data'), 'code/gopath'); + public readonly updateFrequencyMs: number = this.options.updateFrequencyMs; public readonly indexFrequencyMs: number = this.options.indexFrequencyMs; @@ -43,14 +50,14 @@ export class ServerOptions { public readonly maxWorkspace: number = this.options.maxWorkspace; - public readonly disableIndexScheduler: boolean = this.options.disableIndexScheduler; - public readonly enableGlobalReference: boolean = this.options.enableGlobalReference; public readonly lsp: LspOptions = this.options.lsp; public readonly security: SecurityOptions = this.options.security; + public readonly disk: DiskOptions = this.options.disk; + public readonly repoConfigs: RepoConfigs = (this.options.repos as RepoConfig[]).reduce( (previous, current) => { previous[current.repo] = current; diff --git a/x-pack/legacy/plugins/code/server/test_utils.ts b/x-pack/legacy/plugins/code/server/test_utils.ts index 00ab4cc2deff..02ef6b3c687f 100644 --- a/x-pack/legacy/plugins/code/server/test_utils.ts +++ b/x-pack/legacy/plugins/code/server/test_utils.ts @@ -11,6 +11,7 @@ import path from 'path'; import { AnyObject } from './lib/esqueue'; import { ServerOptions } from './server_options'; +import { ServerFacade } from '..'; // TODO migrate other duplicate classes, functions @@ -36,9 +37,12 @@ const TEST_OPTIONS = { enableGitCertCheck: true, gitProtocolWhitelist: ['ssh', 'https', 'git'], }, + disk: { + thresholdEnabled: true, + watermarkLowMb: 100, + }, repos: [], maxWorkspace: 5, // max workspace folder for each language server - disableIndexScheduler: true, // Temp option to disable index scheduler. }; export function createTestServerOption() { @@ -56,7 +60,7 @@ export function createTestServerOption() { } export function createTestHapiServer() { - const server = new Server(); + const server: ServerFacade = new Server(); // @ts-ignore server.config = () => { return { diff --git a/x-pack/legacy/plugins/code/server/utils/esclient_with_internal_request.ts b/x-pack/legacy/plugins/code/server/utils/esclient_with_internal_request.ts index 3a134ebb65cf..5a2cb0952e4b 100644 --- a/x-pack/legacy/plugins/code/server/utils/esclient_with_internal_request.ts +++ b/x-pack/legacy/plugins/code/server/utils/esclient_with_internal_request.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; +import { ServerFacade } from '../..'; import { AnyObject, EsClient } from '../lib/esqueue'; import { EsIndexClient } from './es_index_client'; import { WithInternalRequest } from './with_internal_request'; @@ -12,7 +12,7 @@ import { WithInternalRequest } from './with_internal_request'; export class EsClientWithInternalRequest extends WithInternalRequest implements EsClient { public readonly indices = new EsIndexClient(this); - constructor(server: Server) { + constructor(server: ServerFacade) { super(server); } diff --git a/x-pack/legacy/plugins/code/server/utils/esclient_with_request.ts b/x-pack/legacy/plugins/code/server/utils/esclient_with_request.ts index 85249b4344a0..a1f70db0a707 100644 --- a/x-pack/legacy/plugins/code/server/utils/esclient_with_request.ts +++ b/x-pack/legacy/plugins/code/server/utils/esclient_with_request.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'hapi'; +import { RequestFacade } from '../../'; import { AnyObject, EsClient } from '../lib/esqueue'; import { EsIndexClient } from './es_index_client'; import { WithRequest } from './with_request'; @@ -12,7 +12,7 @@ import { WithRequest } from './with_request'; export class EsClientWithRequest extends WithRequest implements EsClient { public readonly indices = new EsIndexClient(this); - constructor(readonly req: Request) { + constructor(readonly req: RequestFacade) { super(req); } diff --git a/x-pack/legacy/plugins/code/server/utils/server_logger_factory.ts b/x-pack/legacy/plugins/code/server/utils/server_logger_factory.ts index ad45b0d5be56..62a7d197e419 100644 --- a/x-pack/legacy/plugins/code/server/utils/server_logger_factory.ts +++ b/x-pack/legacy/plugins/code/server/utils/server_logger_factory.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Hapi from 'hapi'; import { Logger } from '../log'; import { LoggerFactory } from './log_factory'; +import { ServerFacade } from '../..'; export class ServerLoggerFactory implements LoggerFactory { - constructor(private readonly server: Hapi.Server) {} + constructor(private readonly server: ServerFacade) {} public getLogger(tags: string[]): Logger { return new Logger(this.server, tags); diff --git a/x-pack/legacy/plugins/code/server/utils/with_internal_request.ts b/x-pack/legacy/plugins/code/server/utils/with_internal_request.ts index efccc4c7d0cd..a51fa990ff10 100644 --- a/x-pack/legacy/plugins/code/server/utils/with_internal_request.ts +++ b/x-pack/legacy/plugins/code/server/utils/with_internal_request.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; +import { ServerFacade } from '../..'; import { AnyObject } from '../lib/esqueue'; export class WithInternalRequest { public readonly callCluster: (endpoint: string, clientOptions?: AnyObject) => Promise; - constructor(server: Server) { + constructor(server: ServerFacade) { const cluster = server.plugins.elasticsearch.getCluster('admin'); this.callCluster = cluster.callWithInternalUser; } diff --git a/x-pack/legacy/plugins/code/server/utils/with_request.ts b/x-pack/legacy/plugins/code/server/utils/with_request.ts index fe049d044d4d..e08b9727f375 100644 --- a/x-pack/legacy/plugins/code/server/utils/with_request.ts +++ b/x-pack/legacy/plugins/code/server/utils/with_request.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'hapi'; +import { RequestFacade } from '../../'; import { AnyObject } from '../lib/esqueue'; export class WithRequest { public readonly callCluster: (endpoint: string, clientOptions?: AnyObject) => Promise; - constructor(readonly req: Request) { + constructor(readonly req: RequestFacade) { const cluster = req.server.plugins.elasticsearch.getCluster('data'); // @ts-ignore diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js index de0a89153e2b..61ca7359efbd 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js @@ -11,11 +11,15 @@ jest.mock('../services/auto_follow_pattern_validators', () => ({ validateLeaderIndexPattern: jest.fn(), })); -jest.mock('ui/index_patterns/index_patterns.js', () => ({ +jest.mock('../../../../../../../src/legacy/ui/public/index_patterns/_index_pattern', () => ({ + IndexPattern: jest.fn(), +})); + +jest.mock('../../../../../../../src/legacy/ui/public/index_patterns/index_patterns', () => ({ IndexPatterns: jest.fn(), })); -jest.mock('ui/index_patterns/index_patterns_api_client.js', () => ({ +jest.mock('../../../../../../../src/legacy/ui/public/index_patterns/index_patterns_api_client', () => ({ IndexPatternsApiClient: jest.fn(), })); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index dee85c54653b..6638fcbb82b6 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -131,6 +131,28 @@ export class AutoFollowPatternTable extends PureComponent { } ), actions: [ + { + render: ({ name }) => { + const label = i18n.translate('xpack.crossClusterReplication.autoFollowPatternList.table.actionEditDescription', { + defaultMessage: 'Edit auto-follow pattern', + }); + + return ( + + + + ); + }, + }, { render: ({ name }) => { const label = i18n.translate( @@ -160,28 +182,6 @@ export class AutoFollowPatternTable extends PureComponent { ); }, }, - { - render: ({ name }) => { - const label = i18n.translate('xpack.crossClusterReplication.autoFollowPatternList.table.actionEditDescription', { - defaultMessage: 'Edit auto-follow pattern', - }); - - return ( - - - - ); - }, - }, ], width: '100px', }]; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js index 8a5a8290a899..be12bdecf56b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js @@ -7,11 +7,15 @@ import { validateAutoFollowPattern } from './auto_follow_pattern_validators'; -jest.mock('ui/index_patterns/index_patterns.js', () => ({ +jest.mock('../../../../../../../src/legacy/ui/public/index_patterns/_index_pattern', () => ({ + IndexPattern: jest.fn(), +})); + +jest.mock('../../../../../../../src/legacy/ui/public/index_patterns/index_patterns', () => ({ IndexPatterns: jest.fn(), })); -jest.mock('ui/index_patterns/index_patterns_api_client.js', () => ({ +jest.mock('../../../../../../../src/legacy/ui/public/index_patterns/index_patterns_api_client', () => ({ IndexPatternsApiClient: jest.fn(), })); diff --git a/x-pack/legacy/plugins/file_upload/public/components/index_settings.js b/x-pack/legacy/plugins/file_upload/public/components/index_settings.js index 61c0ce841b65..47f8e8ba72c7 100644 --- a/x-pack/legacy/plugins/file_upload/public/components/index_settings.js +++ b/x-pack/legacy/plugins/file_upload/public/components/index_settings.js @@ -112,6 +112,7 @@ export class IndexSettings extends Component { } > ({ text: indexType, @@ -131,6 +132,7 @@ export class IndexSettings extends Component { error={[indexNameError]} > - + {indexDataJson} @@ -100,7 +100,7 @@ export class JsonImportProgress extends Component { /> - + {indexPatternJson} @@ -112,6 +112,7 @@ export class JsonImportProgress extends Component { defaultMessage: 'Further index modifications can be made using\n', })} {fileParsingProgress ? : null} - { const cleanAndValidate = jest.fn(a => a); @@ -61,26 +61,4 @@ describe('parse file', () => { // Confirm preview function called expect(previewFunction.mock.calls.length).toEqual(1); }); - - it('should use object clone for preview function', () => { - const justFinalJson = { - 'type': 'Feature', - 'geometry': { - 'type': 'Polygon', - 'coordinates': [[ - [-104.05, 78.99], - [-87.22, 78.98], - [-86.58, 75.94], - [-104.03, 75.94], - [-104.05, 78.99] - ]] - }, - }; - - jsonPreview(justFinalJson, previewFunction); - // Confirm equal object passed - expect(previewFunction.mock.calls[0][0]).toEqual(justFinalJson); - // Confirm not the same object - expect(previewFunction.mock.calls[0][0]).not.toBe(justFinalJson); - }); }); diff --git a/x-pack/legacy/plugins/file_upload/public/util/geo_processing.js b/x-pack/legacy/plugins/file_upload/public/util/geo_processing.js index c74b0d6456ca..235a02ae409e 100644 --- a/x-pack/legacy/plugins/file_upload/public/util/geo_processing.js +++ b/x-pack/legacy/plugins/file_upload/public/util/geo_processing.js @@ -26,15 +26,18 @@ const DEFAULT_GEO_POINT_MAPPINGS = { const DEFAULT_INGEST_PIPELINE = {}; export function getGeoIndexTypesForFeatures(featureTypes) { - if (!featureTypes || !featureTypes.length) { + const hasNoFeatureType = !featureTypes || !featureTypes.length; + if (hasNoFeatureType) { return []; - } else if (!featureTypes.includes('Point')) { + } + + const isPoint = featureTypes.includes('Point') || featureTypes.includes('MultiPoint'); + if (!isPoint) { return [ES_GEO_FIELD_TYPE.GEO_SHAPE]; - } else if (featureTypes.includes('Point') && featureTypes.length === 1) { + } else if (isPoint && featureTypes.length === 1) { return [ ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE ]; - } else { - return [ ES_GEO_FIELD_TYPE.GEO_SHAPE ]; } + return [ ES_GEO_FIELD_TYPE.GEO_SHAPE ]; } // Reduces & flattens geojson to coordinates and properties (if any) diff --git a/x-pack/legacy/plugins/file_upload/public/util/http_service.js b/x-pack/legacy/plugins/file_upload/public/util/http_service.js index 44a6a0b31c7a..26d46cecb0e5 100644 --- a/x-pack/legacy/plugins/file_upload/public/util/http_service.js +++ b/x-pack/legacy/plugins/file_upload/public/util/http_service.js @@ -4,16 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ - - // service for interacting with the server import chrome from 'ui/chrome'; import { addSystemApiHeader } from 'ui/system_api'; import { i18n } from '@kbn/i18n'; -const FETCH_TIMEOUT = 10000; - export async function http(options) { if(!(options && options.url)) { throw( @@ -40,38 +36,20 @@ export async function http(options) { if (body !== null) { payload.body = body; } - return await fetchWithTimeout(url, payload); + return await doFetch(url, payload); } -async function fetchWithTimeout(url, payload) { - let timedOut = false; - - return new Promise(function (resolve, reject) { - const timeout = setTimeout(function () { - timedOut = true; - reject(new Error( - i18n.translate('xpack.fileUpload.httpService.requestTimedOut', - { defaultMessage: 'Request timed out' })) - ); - }, FETCH_TIMEOUT); - - fetch(url, payload) - .then(resp => { - clearTimeout(timeout); - if (!timedOut) { - resolve(resp); - } - }) - .catch(function (err) { - reject(err); - if (timedOut) return; - }); - }).then(resp => resp.json()) - .catch(function (err) { - console.error( +async function doFetch(url, payload) { + try { + const resp = await fetch(url, payload); + return resp.json(); + } catch(err) { + return { + failures: [ i18n.translate('xpack.fileUpload.httpService.fetchError', { defaultMessage: 'Error performing fetch: {error}', values: { error: err.message } - })); - }); + })] + }; + } } diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 3f01c73bf626..2f65077226a1 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -26,7 +26,7 @@ import { uiModules } from 'ui/modules'; import uiRoutes from 'ui/routes'; import { addAppRedirectMessageToUrl, fatalError, toastNotifications } from 'ui/notify'; import { formatAngularHttpError } from 'ui/notify/lib'; -import { IndexPatternsProvider } from 'ui/index_patterns/index_patterns'; +import { IndexPatternsProvider } from 'ui/index_patterns'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; import { npStart } from 'ui/new_platform'; @@ -289,7 +289,7 @@ app.controller('graphuiPlugin', function ( $scope.hideAllConfigPanels = function () { $scope.selectedFieldConfig = null; - $scope.kbnTopNav.close(); + $scope.closeMenus(); }; $scope.setAllFieldStatesToDefault = function () { @@ -329,13 +329,12 @@ app.controller('graphuiPlugin', function ( } // Check if user is toggling off an already-open config panel for the current field - if ($scope.kbnTopNav.currentKey === 'fieldConfig' && field === $scope.selectedFieldConfig) { - $scope.hideAllConfigPanels(); + if ($scope.currentlyDisplayedKey === 'fieldConfig' && field === $scope.selectedFieldConfig) { + $scope.currentlyDisplayedKey = null; return; } - $scope.hideAllConfigPanels(); $scope.selectedFieldConfig = field; - $scope.kbnTopNav.currentKey = 'fieldConfig'; + $scope.currentlyDisplayedKey = 'fieldConfig'; }; function canWipeWorkspace(yesFn, noFn) { @@ -498,15 +497,12 @@ app.controller('graphuiPlugin', function ( $scope.clearWorkspace = function () { $scope.workspace = null; $scope.detail = null; - if ($scope.kbnTopNav) { - $scope.kbnTopNav.close(); - } + if ($scope.closeMenus) $scope.closeMenus(); }; $scope.toggleShowAdvancedFieldsConfig = function () { - if ($scope.kbnTopNav.currentKey !== 'fields') { - $scope.kbnTopNav.close(); - $scope.kbnTopNav.currentKey = 'fields'; + if ($scope.currentlyDisplayedKey !== 'fields') { + $scope.currentlyDisplayedKey = 'fields'; //Default the selected field $scope.selectedField = null; $scope.filteredFields = $scope.allFields.filter(function (fieldDef) { @@ -516,7 +512,7 @@ app.controller('graphuiPlugin', function ( $scope.selectedField = $scope.filteredFields[0]; } } else { - $scope.hideAllConfigPanels(); + $scope.currentlyDisplayedKey = undefined; } }; @@ -742,7 +738,7 @@ app.controller('graphuiPlugin', function ( } } - $scope.indices = $route.current.locals.indexPatterns.filter(indexPattern => !indexPattern.get('type')); + $scope.indices = $route.current.locals.indexPatterns.filter(indexPattern => !indexPattern.attributes.type); $scope.setDetail = function (data) { @@ -843,49 +839,48 @@ app.controller('graphuiPlugin', function ( tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { defaultMessage: 'Create a new workspace', }), - run: function () {canWipeWorkspace(function () {kbnUrl.change('/home', {}); }); }, + run: function () { + canWipeWorkspace(function () { + kbnUrl.change('/home', {}); + }); }, }); // if saving is disabled using uiCapabilities, we don't want to render the save // button so it's consistent with all of the other applications if (capabilities.get().graph.save) { // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality - if (!$scope.allSavingDisabled) { - $scope.topNavMenu.push({ - key: 'save', - label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { - defaultMessage: 'Save', - }), - description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { - defaultMessage: 'Save Workspace', - }), - tooltip: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { - defaultMessage: 'Save this workspace', - }), - disableButton: function () {return $scope.selectedFields.length === 0;}, - run: () => { + + $scope.topNavMenu.push({ + key: 'save', + label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { + defaultMessage: 'Save', + }), + description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { + defaultMessage: 'Save Workspace', + }), + tooltip: () => { + if ($scope.allSavingDisabled) { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { + defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', + }); + } else { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { + defaultMessage: 'Save this workspace', + }); + } + }, + disableButton: function () { + return $scope.allSavingDisabled || $scope.selectedFields.length === 0; + }, + run: () => { + $scope.$evalAsync(() => { const curState = $scope.menus.showSave; $scope.closeMenus(); $scope.menus.showSave = !curState; - }, - testId: 'graphSaveButton', - }); - } else { - $scope.topNavMenu.push({ - key: 'save', - label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledLabel', { - defaultMessage: 'Save', - }), - description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledAriaLabel', { - defaultMessage: 'Save Workspace', - }), - tooltip: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { - defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', - }), - disableButton: true, - testId: 'graphSaveButton', - }); - } + }); + }, + testId: 'graphSaveButton', + }); } $scope.topNavMenu.push({ key: 'open', @@ -899,9 +894,11 @@ app.controller('graphuiPlugin', function ( defaultMessage: 'Load a saved workspace', }), run: () => { - const curState = $scope.menus.showLoad; - $scope.closeMenus(); - $scope.menus.showLoad = !curState; + $scope.$evalAsync(() => { + const curState = $scope.menus.showLoad; + $scope.closeMenus(); + $scope.menus.showLoad = !curState; + }); }, testId: 'graphOpenButton', }); @@ -981,9 +978,11 @@ app.controller('graphuiPlugin', function ( defaultMessage: 'Settings', }), run: () => { - const curState = $scope.menus.showSettings; - $scope.closeMenus(); - $scope.menus.showSettings = !curState; + $scope.$evalAsync(() => { + const curState = $scope.menus.showSettings; + $scope.closeMenus(); + $scope.menus.showSettings = !curState; + }); }, }); @@ -994,7 +993,7 @@ app.controller('graphuiPlugin', function ( }; $scope.closeMenus = () => { - _.forOwn($scope.menus, function (value, key) { + _.forOwn($scope.menus, function (_, key) { $scope.menus[key] = false; }); }; @@ -1242,7 +1241,7 @@ app.controller('graphuiPlugin', function ( $scope.savedWorkspace.save().then(function (id) { - $scope.kbnTopNav.close('save'); + $scope.closeMenus(); $scope.userHasConfirmedSaveWorkspaceData = false; //reset flag if (id) { const title = i18n.translate('xpack.graph.saveWorkspace.successNotificationTitle', { diff --git a/x-pack/legacy/plugins/graph/public/templates/_graph.scss b/x-pack/legacy/plugins/graph/public/templates/_graph.scss index b8a6213aef74..23adcbd87dc5 100644 --- a/x-pack/legacy/plugins/graph/public/templates/_graph.scss +++ b/x-pack/legacy/plugins/graph/public/templates/_graph.scss @@ -13,11 +13,18 @@ * 1. Calculated px values come from the open/closed state of the global nav sidebar */ + + #graphBasic { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + } + .gphGraph__container { background: $euiColorEmptyShade; - position: fixed; - height: 100%; - width: calc(100% - #{$kbnGlobalNavClosedWidth}); /* 1 */ + position: relative; + flex: 1; } .kbnGlobalNav.kbnGlobalNav-isOpen + .app-wrapper .gphGraph__container { @@ -72,7 +79,7 @@ } .gphGraph__menus, .gphGraph__bar { - margin: $euiSizeM; + margin: $euiSizeM $euiSizeM 0 $euiSizeM; } .gphGraph__flexGroup { diff --git a/x-pack/legacy/plugins/graph/public/templates/index.html b/x-pack/legacy/plugins/graph/public/templates/index.html index b65dadbf54bc..42679b05052b 100644 --- a/x-pack/legacy/plugins/graph/public/templates/index.html +++ b/x-pack/legacy/plugins/graph/public/templates/index.html @@ -1,9 +1,9 @@
    - - + + -
    +
    @@ -31,7 +31,7 @@ } }}" > - {{f.icon.code}} @@ -74,7 +74,7 @@
    -
    +
    -
    +
    diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js index fbc00604ea3a..8ce3d9f2552d 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/policy_json_flyout.js @@ -5,20 +5,17 @@ */ import React, { PureComponent } from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import PropTypes from 'prop-types'; -import { toastNotifications } from 'ui/notify'; -import copy from 'copy-to-clipboard'; import { - EuiButton, EuiCodeBlock, - EuiFlyoutBody, - EuiFlyoutFooter, EuiFlyout, + EuiFlyoutBody, EuiFlyoutHeader, EuiPortal, + EuiSpacer, + EuiText, EuiTitle, } from '@elastic/eui'; @@ -27,56 +24,61 @@ export class PolicyJsonFlyout extends PureComponent { close: PropTypes.func.isRequired, lifecycle: PropTypes.object.isRequired, }; + getEsJson({ phases }) { return JSON.stringify({ policy: { phases } - }, null, 4); - } - copyToClipboard(lifecycle) { - copy(this.getEsJson(lifecycle)); - toastNotifications.add(i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.policyJsonFlyout.copiedToClipboardMessage', - { - defaultMessage: 'JSON copied to clipboard' - } - )); + }, null, 2); } + render() { const { lifecycle, close, policyName } = this.props; + const endpoint = `PUT _ilm/policy/${policyName || ''}`; + const request = `${endpoint}\n${this.getEsJson(lifecycle)}`; return ( - +

    - + {policyName ? ( + + ) : ( + + )}

    + +

    + +

    +
    + + + - {this.getEsJson(lifecycle)} + {request}
    - - - this.copyToClipboard(lifecycle)}> - - -
    ); diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js index 3daf354bfca0..a7f7cf02550d 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/edit_policy.js @@ -336,7 +336,7 @@ export class EditPolicy extends Component { diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts index efeadc75253c..d2857d5f5f54 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts @@ -10,6 +10,7 @@ import { TestBed, TestBedConfig, findTestSubject, + nextTick, } from '../../../../../../test_utils'; import { IndexManagementHome } from '../../../public/sections/home'; import { BASE_PATH } from '../../../common/constants'; @@ -28,9 +29,12 @@ const initTestBed = registerTestBed(IndexManagementHome, testBedConfig); export interface IdxMgmtHomeTestBed extends TestBed { actions: { - selectTab: (tab: 'indices' | 'index templates') => void; + selectHomeTab: (tab: 'indices' | 'index templates') => void; + selectDetailsTab: (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => void; clickReloadButton: () => void; clickTemplateActionAt: (index: number, action: 'delete') => void; + clickTemplateAt: (index: number) => void; + clickCloseDetailsButton: () => void; }; } @@ -41,7 +45,7 @@ export const setup = async (): Promise => { * User Actions */ - const selectTab = (tab: 'indices' | 'index templates') => { + const selectHomeTab = (tab: 'indices' | 'index templates') => { const tabs = ['indices', 'index templates']; testBed @@ -50,6 +54,15 @@ export const setup = async (): Promise => { .simulate('click'); }; + const selectDetailsTab = (tab: 'summary' | 'settings' | 'mappings' | 'aliases') => { + const tabs = ['summary', 'settings', 'mappings', 'aliases']; + + testBed + .find('templateDetails.tab') + .at(tabs.indexOf(tab)) + .simulate('click'); + }; + const clickReloadButton = () => { const { find } = testBed; find('reloadButton').simulate('click'); @@ -69,12 +82,35 @@ export const setup = async (): Promise => { }); }; + const clickTemplateAt = async (index: number) => { + const { component, table, router } = testBed; + const { rows } = table.getMetaData('templatesTable'); + const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + const { href } = templateLink.props(); + router.navigateTo(href!); + await nextTick(); + component.update(); + }); + }; + + const clickCloseDetailsButton = () => { + const { find } = testBed; + + find('closeDetailsButton').simulate('click'); + }; + return { ...testBed, actions: { - selectTab, + selectHomeTab, + selectDetailsTab, clickReloadButton, clickTemplateActionAt, + clickTemplateAt, + clickCloseDetailsButton, }, }; }; @@ -82,19 +118,31 @@ export const setup = async (): Promise => { type IdxMgmtTestSubjects = TestSubjects; export type TestSubjects = + | 'aliasesTab' | 'appTitle' | 'cell' + | 'closeDetailsButton' | 'deleteSystemTemplateCallOut' | 'deleteTemplateButton' | 'deleteTemplatesButton' | 'deleteTemplatesConfirmation' | 'documentationLink' | 'emptyPrompt' + | 'mappingsTab' | 'indicesList' | 'reloadButton' | 'row' + | 'sectionError' | 'sectionLoading' + | 'settingsTab' + | 'summaryTab' + | 'summaryTitle' | 'systemTemplatesSwitch' | 'tab' + | 'templateDetails' + | 'templateDetails.deleteTemplateButton' + | 'templateDetails.sectionLoading' + | 'templateDetails.tab' + | 'templateDetails.title' | 'templatesList' | 'templatesTable'; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index ddcdc31b3159..90320740d09a 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -36,10 +36,22 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadTemplateResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_PATH}/templates/:id`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadTemplatesResponse, setLoadIndicesResponse, setDeleteTemplateResponse, + setLoadTemplateResponse, }; }; diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts index a66168c51e44..fbd0f6419979 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts @@ -80,7 +80,7 @@ describe.skip('', () => { httpRequestsMockHelpers.setLoadTemplatesResponse([]); - actions.selectTab('index templates'); + actions.selectHomeTab('index templates'); // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { @@ -100,7 +100,7 @@ describe.skip('', () => { httpRequestsMockHelpers.setLoadTemplatesResponse([]); - actions.selectTab('index templates'); + actions.selectHomeTab('index templates'); // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { @@ -146,7 +146,7 @@ describe.skip('', () => { httpRequestsMockHelpers.setLoadTemplatesResponse(templates); - actions.selectTab('index templates'); + actions.selectHomeTab('index templates'); // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { @@ -220,6 +220,16 @@ describe.skip('', () => { expect(updatedRows.length).toEqual(templates.length); }); + test('each row should have a link to the template', async () => { + const { find, exists, actions } = testBed; + + await actions.clickTemplateAt(0); + + expect(exists('templatesList')).toBe(true); + expect(exists('templateDetails')).toBe(true); + expect(find('templateDetails.title').text()).toBe(template1.name); + }); + describe('delete index template', () => { test('should have action buttons on each row to delete an index template', () => { const { table } = testBed; @@ -298,6 +308,221 @@ describe.skip('', () => { expect(latestRequest.url).toBe(`${API_PATH}/templates/${template1.name}`); }); }); + + describe('detail flyout', () => { + it('should have a close button and be able to close flyout', async () => { + const template = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + }); + + const { actions, component, exists } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(template); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('closeDetailsButton')).toBe(true); + expect(exists('summaryTab')).toBe(true); + + actions.clickCloseDetailsButton(); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('summaryTab')).toBe(false); + }); + + it('should have a delete button', async () => { + const template = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + }); + + const { actions, component, exists } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(template); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('templateDetails.deleteTemplateButton')).toBe(true); + }); + + it('should render an error if error fetching template details', async () => { + const { actions, component, exists } = testBed; + const error = { + status: 404, + error: 'Not found', + message: 'Template not found', + }; + + httpRequestsMockHelpers.setLoadTemplateResponse(undefined, { body: error }); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('sectionError')).toBe(true); + // Delete button should not render if error + expect(exists('templateDetails.deleteTemplateButton')).toBe(false); + }); + + describe('tabs', () => { + test('should have 4 tabs if template has mappings, settings and aliases data', async () => { + const template = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + settings: { + index: { + number_of_shards: '1', + }, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + aliases: { + alias1: {}, + }, + }); + + const { find, actions, exists, component } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(template); + + await actions.clickTemplateAt(0); + + expect(find('templateDetails.tab').length).toBe(4); + expect(find('templateDetails.tab').map(t => t.text())).toEqual([ + 'Summary', + 'Settings', + 'Mappings', + 'Aliases', + ]); + + // Summary tab should be initial active tab + expect(exists('summaryTab')).toBe(true); + + // Navigate and verify all tabs + actions.selectDetailsTab('settings'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTab')).toBe(true); + + actions.selectDetailsTab('aliases'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTab')).toBe(false); + expect(exists('aliasesTab')).toBe(true); + + actions.selectDetailsTab('mappings'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(exists('summaryTab')).toBe(false); + expect(exists('settingsTab')).toBe(false); + expect(exists('aliasesTab')).toBe(false); + expect(exists('mappingsTab')).toBe(true); + }); + + it('should not show tabs if mappings, settings and aliases data is not present', async () => { + const templateWithNoTabs = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + }); + + const { actions, find, exists, component } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(templateWithNoTabs); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(find('templateDetails.tab').length).toBe(0); + expect(exists('summaryTab')).toBe(true); + expect(exists('summaryTitle')).toBe(true); + }); + + it('should not show all tabs if mappings, settings or aliases data is not present', async () => { + const templateWithSomeTabs = fixtures.getTemplate({ + name: `a${getRandomString()}`, + indexPatterns: ['template1Pattern1*', 'template1Pattern2'], + settings: { + index: { + number_of_shards: '1', + }, + }, + }); + + const { actions, find, exists, component } = testBed; + + httpRequestsMockHelpers.setLoadTemplateResponse(templateWithSomeTabs); + + await actions.clickTemplateAt(0); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(find('templateDetails.tab').length).toBe(2); + expect(exists('summaryTab')).toBe(true); + // Template does not contain aliases or mappings, so tabs will not render + expect(find('templateDetails.tab').map(t => t.text())).toEqual([ + 'Summary', + 'Settings', + ]); + }); + }); + }); }); }); }); diff --git a/x-pack/legacy/plugins/index_management/common/constants/index.ts b/x-pack/legacy/plugins/index_management/common/constants/index.ts index 9a122ecee63e..06159fd45ede 100644 --- a/x-pack/legacy/plugins/index_management/common/constants/index.ts +++ b/x-pack/legacy/plugins/index_management/common/constants/index.ts @@ -40,4 +40,9 @@ export { UIM_TEMPLATE_LIST_LOAD, UIM_TEMPLATE_DELETE, UIM_TEMPLATE_DELETE_MANY, + UIM_TEMPLATE_SHOW_DETAILS_CLICK, + UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, } from './ui_metric'; diff --git a/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts b/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts index 962b39bfab19..7f0c62ddf5ec 100644 --- a/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts +++ b/x-pack/legacy/plugins/index_management/common/constants/ui_metric.ts @@ -36,3 +36,8 @@ export const UIM_DETAIL_PANEL_SUMMARY_TAB = 'detail_panel_summary_tab'; export const UIM_TEMPLATE_LIST_LOAD = 'template_list_load'; export const UIM_TEMPLATE_DELETE = 'template_delete'; export const UIM_TEMPLATE_DELETE_MANY = 'template_delete_many'; +export const UIM_TEMPLATE_SHOW_DETAILS_CLICK = 'template_show_details_click'; +export const UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB = 'template_details_summary_tab'; +export const UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB = 'template_details_settings_tab'; +export const UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB = 'template_details_mappings_tab'; +export const UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB = 'template_details_aliases_tab'; diff --git a/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx b/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx index cb3ac528ba68..926be6273926 100644 --- a/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx +++ b/x-pack/legacy/plugins/index_management/public/components/delete_templates_modal.tsx @@ -21,10 +21,6 @@ export const DeleteTemplatesModal = ({ }) => { const numTemplatesToDelete = templatesToDelete.length; - if (!numTemplatesToDelete) { - return null; - } - const hasSystemTemplate = Boolean( templatesToDelete.find(templateName => templateName.startsWith('.')) ); diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx index 2784f049ba02..5d2fb54216da 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx +++ b/x-pack/legacy/plugins/index_management/public/sections/home/home.tsx @@ -104,7 +104,7 @@ export const IndexManagementHome: React.FunctionComponent - + diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/index.ts b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/index.ts new file mode 100644 index 000000000000..7360c96c250c --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/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 { TemplateDetails } from './template_details'; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/aliases_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/aliases_tab.tsx new file mode 100644 index 000000000000..02cb59619aae --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/aliases_tab.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCodeEditor } from '@elastic/eui'; +import { Template } from '../../../../../../common/types'; + +interface Props { + templateDetails: Template; +} + +export const AliasesTab: React.FunctionComponent = ({ templateDetails }) => { + const { aliases } = templateDetails; + const aliasesJsonString = JSON.stringify(aliases, null, 2); + + return ( +
    + +
    + ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/index.ts b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/index.ts new file mode 100644 index 000000000000..a648a7f47631 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SummaryTab } from './summary_tab'; +export { MappingsTab } from './mappings_tab'; +export { SettingsTab } from './settings_tab'; +export { AliasesTab } from './aliases_tab'; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/mappings_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/mappings_tab.tsx new file mode 100644 index 000000000000..15133e595a28 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/mappings_tab.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCodeEditor } from '@elastic/eui'; +import { Template } from '../../../../../../common/types'; + +interface Props { + templateDetails: Template; +} + +export const MappingsTab: React.FunctionComponent = ({ templateDetails }) => { + const { mappings } = templateDetails; + const mappingsJsonString = JSON.stringify(mappings, null, 2); + + return ( +
    + +
    + ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/settings_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/settings_tab.tsx new file mode 100644 index 000000000000..697c2e4ab527 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/settings_tab.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCodeEditor } from '@elastic/eui'; +import { Template } from '../../../../../../common/types'; + +interface Props { + templateDetails: Template; +} + +export const SettingsTab: React.FunctionComponent = ({ templateDetails }) => { + const { settings } = templateDetails; + const settingsJsonString = JSON.stringify(settings, null, 2); + + return ( +
    + +
    + ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/summary_tab.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/summary_tab.tsx new file mode 100644 index 000000000000..3d55bdf6ab97 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/tabs/summary_tab.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { Template } from '../../../../../../common/types'; +import { getILMPolicyPath } from '../../../../../services/navigation'; + +interface Props { + templateDetails: Template; +} + +const NoneDescriptionText = () => ( + +); + +export const SummaryTab: React.FunctionComponent = ({ templateDetails }) => { + const { version, order, indexPatterns = [], settings } = templateDetails; + + const ilmPolicy = settings && settings.index && settings.index.lifecycle; + const numIndexPatterns = indexPatterns.length; + + return ( + + + + + + + + {numIndexPatterns > 1 ? ( + +
      + {indexPatterns.map((indexName: string, i: number) => { + return ( +
    • + + {indexName} + +
    • + ); + })} +
    +
    + ) : ( + indexPatterns.toString() + )} +
    +
    +
    + + + + + + + + {ilmPolicy && ilmPolicy.name ? ( + {ilmPolicy.name} + ) : ( + + )} + + + + + + {typeof order !== 'undefined' ? order : } + + + + + + {typeof version !== 'undefined' ? version : } + + + +
    + ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/template_details.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/template_details.tsx new file mode 100644 index 000000000000..9c295fbcbc2d --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/template_details/template_details.tsx @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiTab, + EuiTabs, + EuiSpacer, +} from '@elastic/eui'; +import { + UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +} from '../../../../../common/constants'; +import { Template } from '../../../../../common/types'; +import { DeleteTemplatesModal, SectionLoading, SectionError } from '../../../../components'; +import { loadIndexTemplate } from '../../../../services/api'; +import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric'; +import { SummaryTab, MappingsTab, SettingsTab, AliasesTab } from './tabs'; + +interface Props { + templateName: Template['name']; + onClose: () => void; + reload: () => Promise; +} + +const SUMMARY_TAB_ID = 'summary'; +const MAPPINGS_TAB_ID = 'mappings'; +const ALIASES_TAB_ID = 'aliases'; +const SETTINGS_TAB_ID = 'settings'; + +const summaryTabData = { + id: SUMMARY_TAB_ID, + name: ( + + ), +}; + +const settingsTabData = { + id: SETTINGS_TAB_ID, + name: ( + + ), +}; + +const mappingsTabData = { + id: MAPPINGS_TAB_ID, + name: ( + + ), +}; + +const aliasesTabData = { + id: ALIASES_TAB_ID, + name: ( + + ), +}; + +const tabToComponentMap: { + [key: string]: React.FunctionComponent<{ templateDetails: Template }>; +} = { + [SUMMARY_TAB_ID]: SummaryTab, + [SETTINGS_TAB_ID]: SettingsTab, + [MAPPINGS_TAB_ID]: MappingsTab, + [ALIASES_TAB_ID]: AliasesTab, +}; + +const tabToUiMetricMap: { [key: string]: string } = { + [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +}; + +const hasEntries = (tabData: object) => { + return tabData ? Object.entries(tabData).length > 0 : false; +}; + +export const TemplateDetails: React.FunctionComponent = ({ + templateName, + onClose, + reload, +}) => { + const { error, data: templateDetails, isLoading } = loadIndexTemplate(templateName); + + const [templateToDelete, setTemplateToDelete] = useState>([]); + const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error} + data-test-subj="sectionError" + /> + ); + } else if (templateDetails) { + const { settings, mappings, aliases } = templateDetails; + + const settingsTab = hasEntries(settings) ? [settingsTabData] : []; + const mappingsTab = hasEntries(mappings) ? [mappingsTabData] : []; + const aliasesTab = hasEntries(aliases) ? [aliasesTabData] : []; + + const optionalTabs = [...settingsTab, ...mappingsTab, ...aliasesTab]; + const tabs = optionalTabs.length > 0 ? [summaryTabData, ...optionalTabs] : []; + + if (tabs.length > 0) { + const Content = tabToComponentMap[activeTab]; + + content = ( + + + {tabs.map(tab => ( + { + trackUiMetric(METRIC_TYPE.CLICK, tabToUiMetricMap[tab.id]); + setActiveTab(tab.id); + }} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subj="tab" + > + {tab.name} + + ))} + + + + + + + ); + } else { + content = ( + + +

    + +

    +
    + + + + +
    + ); + } + } + + return ( + + {templateToDelete.length ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } + setTemplateToDelete([]); + onClose(); + }} + templatesToDelete={templateToDelete} + /> + ) : null} + + + + +

    + {templateName} +

    +
    +
    + + {content} + + + + + + + + + + {templateDetails && ( + + + + + + )} + + +
    +
    + ); +}; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx index a17bfaf1ce27..51fa92582d57 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx @@ -5,6 +5,7 @@ */ import React, { Fragment, useState, useEffect, useMemo } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt, @@ -20,9 +21,19 @@ import { TemplatesTable } from './templates_table'; import { loadIndexTemplates } from '../../../services/api'; import { Template } from '../../../../common/types'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; -import { UIM_TEMPLATE_LIST_LOAD } from '../../../../common/constants'; +import { UIM_TEMPLATE_LIST_LOAD, BASE_PATH } from '../../../../common/constants'; +import { TemplateDetails } from './template_details'; -export const TemplatesList: React.FunctionComponent = () => { +interface MatchParams { + templateName?: Template['name']; +} + +export const TemplatesList: React.FunctionComponent> = ({ + match: { + params: { templateName }, + }, + history, +}) => { const { error, isLoading, data: templates, createRequest: reload } = loadIndexTemplates(); let content; @@ -36,6 +47,10 @@ export const TemplatesList: React.FunctionComponent = () => { [templates] ); + const closeTemplateDetails = () => { + history.push(`${BASE_PATH}templates`); + }; + // Track component loaded useEffect(() => { trackUiMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD); @@ -115,5 +130,16 @@ export const TemplatesList: React.FunctionComponent = () => { ); } - return
    {content}
    ; + return ( +
    + {content} + {templateName && ( + + )} +
    + ); }; diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx index c237d26021be..8025b89f9d9f 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_table/templates_table.tsx @@ -7,9 +7,18 @@ import React, { useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiInMemoryTable, EuiIcon, EuiButton, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { + EuiInMemoryTable, + EuiIcon, + EuiButton, + EuiToolTip, + EuiButtonIcon, + EuiLink, +} from '@elastic/eui'; import { Template } from '../../../../../common/types'; +import { BASE_PATH, UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../common/constants'; import { DeleteTemplatesModal } from '../../../../components'; +import { trackUiMetric, METRIC_TYPE } from '../../../../services/track_ui_metric'; interface Props { templates: Template[]; @@ -34,6 +43,17 @@ export const TemplatesTable: React.FunctionComponent = ({ templates, relo }), truncateText: true, sortable: true, + render: (name: Template['name']) => { + return ( + trackUiMetric(METRIC_TYPE.CLICK, UIM_TEMPLATE_SHOW_DETAILS_CLICK)} + > + {name} + + ); + }, }, { field: 'indexPatterns', @@ -206,15 +226,17 @@ export const TemplatesTable: React.FunctionComponent = ({ templates, relo return ( - { - if (data && data.hasDeletedTemplates) { - reload(); - } - setTemplatesToDelete([]); - }} - templatesToDelete={templatesToDelete} - /> + {templatesToDelete.length ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } + setTemplatesToDelete([]); + }} + templatesToDelete={templatesToDelete} + /> + ) : null} ) => { uimActionType, }); }; + +export function loadIndexTemplate(name: Template['name']) { + return useRequest({ + path: `${apiPrefix}/templates/${encodeURIComponent(name)}`, + method: 'get', + }); +} diff --git a/x-pack/legacy/plugins/index_management/public/services/navigation.js b/x-pack/legacy/plugins/index_management/public/services/navigation.js deleted file mode 100644 index 45e4c73d8643..000000000000 --- a/x-pack/legacy/plugins/index_management/public/services/navigation.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { BASE_PATH } from '../../common/constants'; -let urlService; -export const setUrlService = (aUrlService) => { - urlService = aUrlService; -}; -export const getUrlService = () => { - return urlService; -}; -export const getIndexListUri = (filter) => { - if(filter) { - // React router tries to decode url params but it can't because the browser partially - // decodes them. So we have to encode both the URL and the filter to get it all to - // work correctly for filters with URL unsafe characters in them. - return encodeURI(`#${BASE_PATH}indices/filter/${encodeURIComponent(filter)}`); - } - - // If no filter, URI is already safe so no need to encode. - return `#${BASE_PATH}indices`; -}; diff --git a/x-pack/legacy/plugins/index_management/public/services/navigation.ts b/x-pack/legacy/plugins/index_management/public/services/navigation.ts new file mode 100644 index 000000000000..16072cb702f0 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/services/navigation.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. + */ +import { BASE_PATH } from '../../common/constants'; + +let urlService: any; + +export const setUrlService = (aUrlService: any) => { + urlService = aUrlService; +}; + +export const getUrlService = () => { + return urlService; +}; + +export const getIndexListUri = (filter: any) => { + if (filter) { + // React router tries to decode url params but it can't because the browser partially + // decodes them. So we have to encode both the URL and the filter to get it all to + // work correctly for filters with URL unsafe characters in them. + return encodeURI(`#${BASE_PATH}indices/filter/${encodeURIComponent(filter)}`); + } + + // If no filter, URI is already safe so no need to encode. + return `#${BASE_PATH}indices`; +}; + +export const getILMPolicyPath = (policyName: string) => { + return encodeURI( + `#/management/elasticsearch/index_lifecycle_management/policies/edit/${encodeURIComponent( + policyName + )}` + ); +}; diff --git a/x-pack/legacy/plugins/index_management/server/lib/fetch_templates.ts b/x-pack/legacy/plugins/index_management/server/lib/fetch_templates.ts deleted file mode 100644 index 673c4569f21c..000000000000 --- a/x-pack/legacy/plugins/index_management/server/lib/fetch_templates.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const fetchTemplates = async (callWithRequest: any) => { - const indexTemplatesByName = await callWithRequest('indices.getTemplate'); - const indexTemplateNames = Object.keys(indexTemplatesByName); - - const indexTemplates = indexTemplateNames.map(name => { - const { - version, - order, - index_patterns: indexPatterns = [], - settings = {}, - aliases = {}, - mappings = {}, - } = indexTemplatesByName[name]; - return { - name, - version, - order, - indexPatterns: indexPatterns.sort(), - settings, - aliases, - mappings, - }; - }); - - return indexTemplates; -}; diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.ts new file mode 100644 index 000000000000..7ed94ba73b13 --- /dev/null +++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_get_routes.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 { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router'; + +const allHandler: RouterRouteHandler = async (_req, callWithRequest) => { + const indexTemplatesByName = await callWithRequest('indices.getTemplate'); + const indexTemplateNames = Object.keys(indexTemplatesByName); + + const indexTemplates = indexTemplateNames.map(name => { + const { + version, + order, + index_patterns: indexPatterns = [], + settings = {}, + aliases = {}, + mappings = {}, + } = indexTemplatesByName[name]; + return { + name, + version, + order, + indexPatterns: indexPatterns.sort(), + settings, + aliases, + mappings, + }; + }); + + return indexTemplates; +}; + +const oneHandler: RouterRouteHandler = async (req, callWithRequest) => { + const { name } = req.params; + const indexTemplateByName = await callWithRequest('indices.getTemplate', { name }); + + if (indexTemplateByName[name]) { + const { + version, + order, + index_patterns: indexPatterns = [], + settings = {}, + aliases = {}, + mappings = {}, + } = indexTemplateByName[name]; + + return { + name, + version, + order, + indexPatterns: indexPatterns.sort(), + settings, + aliases, + mappings, + }; + } +}; + +export function registerGetAllRoute(router: Router) { + router.get('templates', allHandler); +} + +export function registerGetOneRoute(router: Router) { + router.get('templates/{name}', oneHandler); +} diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_list_route.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_list_route.ts deleted file mode 100644 index c951c2d62330..000000000000 --- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_list_route.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 { Router, RouterRouteHandler } from '../../../../../../server/lib/create_router'; -import { fetchTemplates } from '../../../lib/fetch_templates'; - -const handler: RouterRouteHandler = async (_req, callWithRequest) => { - return fetchTemplates(callWithRequest); -}; - -export function registerListRoute(router: Router) { - router.get('templates', handler); -} diff --git a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts index 80b59ef5ce0f..084abd0a91eb 100644 --- a/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts +++ b/x-pack/legacy/plugins/index_management/server/routes/api/templates/register_templates_routes.ts @@ -5,12 +5,13 @@ */ import { Router } from '../../../../../../server/lib/create_router'; -import { registerListRoute } from './register_list_route'; +import { registerGetAllRoute, registerGetOneRoute } from './register_get_routes'; import { registerDeleteRoute } from './register_delete_route'; import { registerCreateRoute } from './register_create_route'; export function registerTemplatesRoutes(router: Router) { - registerListRoute(router); + registerGetAllRoute(router); + registerGetOneRoute(router); registerDeleteRoute(router); registerCreateRoute(router); } diff --git a/x-pack/legacy/plugins/infra/README.md b/x-pack/legacy/plugins/infra/README.md index 6287b686dad2..f13d0a472171 100644 --- a/x-pack/legacy/plugins/infra/README.md +++ b/x-pack/legacy/plugins/infra/README.md @@ -120,7 +120,7 @@ life-cycle of a PR looks like the following: There are always exceptions to the rule, so seeking guidance about any of the steps is highly recommended. -[Kibana's contribution procedures]: ../../../CONTRIBUTING.md +[Kibana's contribution procedures]: ../../../../CONTRIBUTING.md [Infrastructure forum]: https://discuss.elastic.co/c/infrastructure [Logs forum]: https://discuss.elastic.co/c/logs [ECS]: https://github.com/elastic/ecs/ diff --git a/x-pack/legacy/plugins/infra/common/graphql/types.ts b/x-pack/legacy/plugins/infra/common/graphql/types.ts index 2da829dbf293..7843a54b93be 100644 --- a/x-pack/legacy/plugins/infra/common/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/common/graphql/types.ts @@ -28,8 +28,6 @@ export interface InfraSource { configuration: InfraSourceConfiguration; /** The status of the source */ status: InfraSourceStatus; - /** A hierarchy of metadata entries by node */ - metadataByNode: InfraNodeMetadata; /** A consecutive span of log entries surrounding a point in time */ logEntriesAround: InfraLogEntryInterval; /** A consecutive span of log entries within an interval */ @@ -132,20 +130,6 @@ export interface InfraIndexField { /** Whether the field's values can be aggregated */ aggregatable: boolean; } -/** One metadata entry for a node. */ -export interface InfraNodeMetadata { - id: string; - - name: string; - - features: InfraNodeFeature[]; -} - -export interface InfraNodeFeature { - name: string; - - source: string; -} /** A consecutive sequence of log entries */ export interface InfraLogEntryInterval { /** The key corresponding to the start of the interval covered by the entries */ @@ -424,11 +408,6 @@ export interface SourceQueryArgs { /** The id of the source */ id: string; } -export interface MetadataByNodeInfraSourceArgs { - nodeId: string; - - nodeType: InfraNodeType; -} export interface LogEntriesAroundInfraSourceArgs { /** The sort key that corresponds to the point in time */ key: InfraTimeKeyInput; @@ -722,44 +701,6 @@ export namespace LogSummary { }; } -export namespace MetadataQuery { - export type Variables = { - sourceId: string; - nodeId: string; - nodeType: InfraNodeType; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'InfraSource'; - - id: string; - - metadataByNode: MetadataByNode; - }; - - export type MetadataByNode = { - __typename?: 'InfraNodeMetadata'; - - name: string; - - features: Features[]; - }; - - export type Features = { - __typename?: 'InfraNodeFeature'; - - name: string; - - source: string; - }; -} - export namespace MetricsQuery { export type Variables = { sourceId: string; diff --git a/x-pack/legacy/plugins/infra/common/http_api/index.ts b/x-pack/legacy/plugins/infra/common/http_api/index.ts index 90afdcb43ffb..5278ae6c249c 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/index.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './search_results_api'; -export * from './search_summary_api'; +export * from './log_analysis'; +export * from './metadata_api'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts new file mode 100644 index 000000000000..38684cb22e23 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/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 './results'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/index.ts new file mode 100644 index 000000000000..174942127771 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/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 './log_entry_rate'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts new file mode 100644 index 000000000000..2dcaf35cc41d --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.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 * as rt from 'io-ts'; + +import { + badRequestErrorRT, + conflictErrorRT, + forbiddenErrorRT, + metricStatisticsRT, + timeRangeRT, +} from '../../shared'; + +export const LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH = + '/api/infra/log_analysis/results/log_entry_rate'; + +/** + * request + */ + +export const getLogEntryRateRequestPayloadRT = rt.type({ + data: rt.type({ + bucketDuration: rt.number, + sourceId: rt.string, + timeRange: timeRangeRT, + }), +}); + +export type GetLogEntryRateRequestPayload = rt.TypeOf; + +/** + * response + */ + +export const logEntryRateAnomaly = rt.type({ + actualLogEntryRate: rt.number, + anomalyScore: rt.number, + duration: rt.number, + startTime: rt.number, + typicalLogEntryRate: rt.number, +}); + +export const logEntryRateHistogramBucket = rt.type({ + anomalies: rt.array(logEntryRateAnomaly), + duration: rt.number, + logEntryRateStats: metricStatisticsRT, + modelLowerBoundStats: metricStatisticsRT, + modelUpperBoundStats: metricStatisticsRT, + startTime: rt.number, +}); + +export const getLogEntryRateSuccessReponsePayloadRT = rt.type({ + data: rt.type({ + bucketDuration: rt.number, + histogramBuckets: rt.array(logEntryRateHistogramBucket), + }), +}); + +export type GetLogEntryRateSuccessResponsePayload = rt.TypeOf< + typeof getLogEntryRateSuccessReponsePayloadRT +>; + +export const getLogEntryRateResponsePayloadRT = rt.union([ + getLogEntryRateSuccessReponsePayloadRT, + badRequestErrorRT, + conflictErrorRT, + forbiddenErrorRT, +]); + +export type GetLogEntryRateReponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts new file mode 100644 index 000000000000..796960651122 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 rt from 'io-ts'; +import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; + +export const InfraMetadataNodeTypeRT = rt.keyof({ + host: null, + pod: null, + container: null, +}); + +export const InfraMetadataRequestRT = rt.type({ + nodeId: rt.string, + nodeType: InfraMetadataNodeTypeRT, + sourceId: rt.string, +}); + +export const InfraMetadataFeatureRT = rt.type({ + name: rt.string, + source: rt.string, +}); + +export const InfraMetadataOSRT = rt.partial({ + codename: rt.string, + family: rt.string, + kernel: rt.string, + name: rt.string, + platform: rt.string, + version: rt.string, +}); + +export const InfraMetadataHostRT = rt.partial({ + name: rt.string, + os: InfraMetadataOSRT, + architecture: rt.string, + containerized: rt.boolean, +}); + +export const InfraMetadataInstanceRT = rt.partial({ + id: rt.string, + name: rt.string, +}); + +export const InfraMetadataProjectRT = rt.partial({ + id: rt.string, +}); + +export const InfraMetadataMachineRT = rt.partial({ + interface: rt.string, +}); + +export const InfraMetadataCloudRT = rt.partial({ + instance: InfraMetadataInstanceRT, + provider: rt.string, + availability_zone: rt.string, + project: InfraMetadataProjectRT, + machine: InfraMetadataMachineRT, +}); + +export const InfraMetadataInfoRT = rt.partial({ + cloud: InfraMetadataCloudRT, + host: InfraMetadataHostRT, +}); + +const InfraMetadataRequiredRT = rt.type({ + name: rt.string, + features: rt.array(InfraMetadataFeatureRT), +}); + +const InfraMetadataOptionalRT = rt.partial({ + info: InfraMetadataInfoRT, +}); + +export const InfraMetadataRT = rt.intersection([InfraMetadataRequiredRT, InfraMetadataOptionalRT]); + +export type InfraMetadata = rt.TypeOf; + +export type InfraMetadataRequest = rt.TypeOf; + +export type InfraMetadataWrappedRequest = InfraWrappableRequest; + +export type InfraMetadataFeature = rt.TypeOf; + +export type InfraMetadataInfo = rt.TypeOf; + +export type InfraMetadataCloud = rt.TypeOf; + +export type InfraMetadataInstance = rt.TypeOf; + +export type InfraMetadataProject = rt.TypeOf; + +export type InfraMetadataMachine = rt.TypeOf; + +export type InfraMetadataHost = rt.TypeOf; + +export type InfraMEtadataOS = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/http_api/search_results_api.ts b/x-pack/legacy/plugins/infra/common/http_api/search_results_api.ts deleted file mode 100644 index b866a7e1ab08..000000000000 --- a/x-pack/legacy/plugins/infra/common/http_api/search_results_api.ts +++ /dev/null @@ -1,37 +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 { LogEntryFieldsMapping, LogEntryTime } from '../log_entry'; -import { SearchResult } from '../log_search_result'; -import { TimedApiResponse } from './timed_api'; - -interface CommonSearchResultsPostPayload { - indices: string[]; - fields: LogEntryFieldsMapping; - query: string; -} - -export interface AdjacentSearchResultsApiPostPayload extends CommonSearchResultsPostPayload { - target: LogEntryTime; - before: number; - after: number; -} - -export interface AdjacentSearchResultsApiPostResponse extends TimedApiResponse { - results: { - before: SearchResult[]; - after: SearchResult[]; - }; -} - -export interface ContainedSearchResultsApiPostPayload extends CommonSearchResultsPostPayload { - start: LogEntryTime; - end: LogEntryTime; -} - -export interface ContainedSearchResultsApiPostResponse extends TimedApiResponse { - results: SearchResult[]; -} diff --git a/x-pack/legacy/plugins/infra/common/http_api/search_summary_api.ts b/x-pack/legacy/plugins/infra/common/http_api/search_summary_api.ts deleted file mode 100644 index 90e8fa8e78bc..000000000000 --- a/x-pack/legacy/plugins/infra/common/http_api/search_summary_api.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 { LogEntryFieldsMapping } from '../log_entry'; -import { SearchSummaryBucket } from '../log_search_summary'; -import { SummaryBucketSize } from '../log_summary'; -import { TimedApiResponse } from './timed_api'; - -export interface SearchSummaryApiPostPayload { - bucketSize: { - unit: SummaryBucketSize; - value: number; - }; - fields: LogEntryFieldsMapping; - indices: string[]; - start: number; - end: number; - query: string; -} - -export interface SearchSummaryApiPostResponse extends TimedApiResponse { - buckets: SearchSummaryBucket[]; -} diff --git a/x-pack/legacy/plugins/infra/common/http_api/shared/errors.ts b/x-pack/legacy/plugins/infra/common/http_api/shared/errors.ts new file mode 100644 index 000000000000..74608cec4d0d --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/shared/errors.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 * as rt from 'io-ts'; + +const createErrorRuntimeType = ( + statusCode: number, + errorCode: string, + attributes?: Attributes +) => + rt.type({ + statusCode: rt.literal(statusCode), + error: rt.literal(errorCode), + message: rt.string, + ...(!!attributes ? { attributes } : {}), + }); + +export const badRequestErrorRT = createErrorRuntimeType(400, 'Bad Request'); +export const forbiddenErrorRT = createErrorRuntimeType(403, 'Forbidden'); +export const conflictErrorRT = createErrorRuntimeType(409, 'Conflict'); diff --git a/x-pack/legacy/plugins/infra/common/http_api/shared/index.ts b/x-pack/legacy/plugins/infra/common/http_api/shared/index.ts new file mode 100644 index 000000000000..1047ca2f2a01 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/shared/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 './errors'; +export * from './metric_statistics'; +export * from './time_range'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/shared/metric_statistics.ts b/x-pack/legacy/plugins/infra/common/http_api/shared/metric_statistics.ts new file mode 100644 index 000000000000..70bd85402c43 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/shared/metric_statistics.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const metricStatisticsRT = rt.type({ + avg: rt.union([rt.number, rt.null]), + count: rt.number, + max: rt.union([rt.number, rt.null]), + min: rt.union([rt.number, rt.null]), + sum: rt.number, +}); diff --git a/x-pack/legacy/plugins/infra/common/http_api/shared/time_range.ts b/x-pack/legacy/plugins/infra/common/http_api/shared/time_range.ts new file mode 100644 index 000000000000..1a5bb182efa8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/shared/time_range.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const timeRangeRT = rt.type({ + startTime: rt.number, + endTime: rt.number, +}); diff --git a/x-pack/legacy/plugins/infra/common/http_api/timed_api.ts b/x-pack/legacy/plugins/infra/common/http_api/timed_api.ts deleted file mode 100644 index 7d0f227f0d0b..000000000000 --- a/x-pack/legacy/plugins/infra/common/http_api/timed_api.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. - */ - -export interface ApiResponseTimings { - [timing: string]: number; -} - -export interface TimedApiResponse { - timings: ApiResponseTimings; -} diff --git a/x-pack/legacy/plugins/infra/common/log_analysis/index.ts b/x-pack/legacy/plugins/infra/common/log_analysis/index.ts new file mode 100644 index 000000000000..79913f829191 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/log_analysis/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 * from './log_analysis'; +export * from './job_parameters'; diff --git a/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts new file mode 100644 index 000000000000..ff4490d2c700 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.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 { JobType } from './log_analysis'; + +export const getJobId = (spaceId: string, sourceId: string, jobType: JobType) => + `kibana-logs-ui-${spaceId}-${sourceId}-${jobType}`; diff --git a/x-pack/legacy/plugins/infra/common/log_analysis/log_analysis.ts b/x-pack/legacy/plugins/infra/common/log_analysis/log_analysis.ts new file mode 100644 index 000000000000..870d9f50d44d --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/log_analysis/log_analysis.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 * as rt from 'io-ts'; + +export const jobTypeRT = rt.keyof({ + 'log-entry-rate': null, +}); + +export type JobType = rt.TypeOf; + +export const jobStatusRT = rt.keyof({ + created: null, + missing: null, + running: null, +}); + +export type JobStatus = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/runtime_types.ts b/x-pack/legacy/plugins/infra/common/runtime_types.ts new file mode 100644 index 000000000000..297743f9b345 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/runtime_types.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 { Errors } from 'io-ts'; +import { failure } from 'io-ts/lib/PathReporter'; + +export const createPlainError = (message: string) => new Error(message); + +export const throwErrors = (createError: (message: string) => Error) => (errors: Errors) => { + throw createError(failure(errors).join('\n')); +}; diff --git a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index 990ee1c97c2e..940d187f2f3a 100644 --- a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -28,6 +28,7 @@ interface AutocompleteFieldProps { suggestions: AutocompleteSuggestion[]; value: string; autoFocus?: boolean; + 'aria-label'?: string; } interface AutocompleteFieldState { @@ -49,7 +50,14 @@ export class AutocompleteField extends React.Component< private inputElement: HTMLInputElement | null = null; public render() { - const { suggestions, isLoadingSuggestions, isValid, placeholder, value } = this.props; + const { + suggestions, + isLoadingSuggestions, + isValid, + placeholder, + value, + 'aria-label': ariaLabel, + } = this.props; const { areSuggestionsVisible, selectedIndex } = this.state; return ( @@ -65,9 +73,9 @@ export class AutocompleteField extends React.Component< onKeyDown={this.handleKeyDown} onKeyUp={this.handleKeyUp} onSearch={this.submit} - onBlur={this.submit} placeholder={placeholder} value={value} + aria-label={ariaLabel} /> {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx index c49178a92f8d..ba8e6d90ca7a 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; import React from 'react'; @@ -20,20 +19,13 @@ import { LogEntryColumnContent, LogEntryColumnWidth, LogEntryColumnWidths, - iconColumnId, } from './log_entry_column'; import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel'; export const LogColumnHeaders = injectI18n<{ columnConfigurations: LogColumnConfiguration[]; columnWidths: LogEntryColumnWidths; - showColumnConfiguration: () => void; -}>(({ columnConfigurations, columnWidths, intl, showColumnConfiguration }) => { - const showColumnConfigurationLabel = intl.formatMessage({ - id: 'xpack.infra.logColumnHeaders.configureColumnsLabel', - defaultMessage: 'Configure columns', - }); - +}>(({ columnConfigurations, columnWidths, intl }) => { return ( {columnConfigurations.map(columnConfiguration => { @@ -69,19 +61,6 @@ export const LogColumnHeaders = injectI18n<{ ); } })} - - - ); }); diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index c765531f9083..b9a4d4244a1f 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -48,7 +48,6 @@ interface ScrollableLogTextStreamViewProps { loadNewerItems: () => void; setFlyoutItem: (id: string) => void; setFlyoutVisibility: (visible: boolean) => void; - showColumnConfiguration: () => void; intl: InjectedIntl; highlightedItem: string | null; currentHighlightKey: UniqueTimeKey | null; @@ -109,7 +108,6 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent< items, lastLoadedTime, scale, - showColumnConfiguration, wrap, } = this.props; const { targetId } = this.state; @@ -153,7 +151,6 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent< {({ measureRef, bounds: { height = 0 }, content: { width = 0 } }) => ( diff --git a/x-pack/legacy/plugins/infra/public/components/metrics/invalid_node.tsx b/x-pack/legacy/plugins/infra/public/components/metrics/invalid_node.tsx index 60bd51f0ba6d..673bca91904c 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics/invalid_node.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics/invalid_node.tsx @@ -6,19 +6,20 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useContext } from 'react'; +import React from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; +import { + ViewSourceConfigurationButton, + ViewSourceConfigurationButtonHrefBase, +} from '../../components/source_configuration'; interface InvalidNodeErrorProps { nodeName: string; } export const InvalidNodeError: React.FunctionComponent = ({ nodeName }) => { - const { showIndicesConfiguration } = useContext(SourceConfigurationFlyoutState.Context); - return ( {({ basePath }) => ( @@ -57,12 +58,15 @@ export const InvalidNodeError: React.FunctionComponent = - + - + } diff --git a/x-pack/legacy/plugins/infra/public/components/metrics/sections/chart_section.tsx b/x-pack/legacy/plugins/infra/public/components/metrics/sections/chart_section.tsx index 8976d77ea912..547ea08361bf 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics/sections/chart_section.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics/sections/chart_section.tsx @@ -4,9 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback } from 'react'; +import moment from 'moment'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; -import { Axis, Chart, getAxisId, niceTimeFormatter, Position, Settings } from '@elastic/charts'; +import { + Axis, + Chart, + getAxisId, + niceTimeFormatter, + Position, + Settings, + TooltipValue, +} from '@elastic/charts'; import { EuiPageContentBody, EuiTitle } from '@elastic/eui'; import { InfraMetricLayoutSection } from '../../../pages/metrics/layouts/types'; import { InfraMetricData, InfraTimerangeInput } from '../../../graphql/types'; @@ -22,6 +31,7 @@ import { seriesHasLessThen2DataPoints, } from './helpers'; import { ErrorMessage } from './error_message'; +import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; interface Props { section: InfraMetricLayoutSection; @@ -35,6 +45,7 @@ interface Props { export const ChartSection = injectI18n( ({ onChangeRangeTime, section, metric, intl, stopLiveStreaming, isLiveStreaming }: Props) => { const { visConfig } = section; + const [dateFormat] = useKibanaUiSetting('dateFormat'); const formatter = get(visConfig, 'formatter', InfraFormatterType.number); const formatterTemplate = get(visConfig, 'formatterTemplate', '{{value}}'); const valueFormatter = useCallback(getFormatter(formatter, formatterTemplate), [ @@ -57,6 +68,12 @@ export const ChartSection = injectI18n( }, [onChangeRangeTime, isLiveStreaming, stopLiveStreaming] ); + const tooltipProps = { + headerFormatter: useCallback( + (data: TooltipValue) => moment(data.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'), + [dateFormat] + ), + }; if (!metric) { return ( @@ -115,7 +132,11 @@ export const ChartSection = injectI18n( stack={visConfig.stacked} /> ))} - +
    diff --git a/x-pack/legacy/plugins/infra/public/components/metrics/sections/series_chart.tsx b/x-pack/legacy/plugins/infra/public/components/metrics/sections/series_chart.tsx index 10d05885cbd1..ce359eed05a0 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics/sections/series_chart.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics/sections/series_chart.tsx @@ -12,6 +12,9 @@ import { getSpecId, DataSeriesColorsValues, CustomSeriesColorsMap, + RecursivePartial, + BarSeriesStyle, + AreaSeriesStyle, } from '@elastic/charts'; import { InfraMetricLayoutVisualizationType } from '../../../pages/metrics/layouts/types'; import { InfraDataSeries } from '../../../graphql/types'; @@ -33,26 +36,18 @@ export const SeriesChart = (props: Props) => { }; export const AreaChart = ({ id, color, series, name, type, stack }: Props) => { - const style = { + const style: RecursivePartial = { area: { - fill: color, opacity: 1, visible: InfraMetricLayoutVisualizationType.area === type, }, line: { - stroke: color, strokeWidth: InfraMetricLayoutVisualizationType.area === type ? 1 : 2, visible: true, }, - border: { - visible: false, - strokeWidth: 2, - stroke: color, - }, point: { visible: false, radius: 0.2, - stroke: color, strokeWidth: 2, opacity: 1, }, @@ -80,19 +75,13 @@ export const AreaChart = ({ id, color, series, name, type, stack }: Props) => { }; export const BarChart = ({ id, color, series, name, type, stack }: Props) => { - const style = { + const style: RecursivePartial = { rectBorder: { stroke: color, strokeWidth: 1, visible: true, }, - border: { - visible: false, - strokeWidth: 2, - stroke: color, - }, rect: { - fill: color, opacity: 1, }, }; diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx index d3fcd9671acf..7cd8385172c5 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx @@ -7,7 +7,15 @@ import React, { useCallback, useMemo } from 'react'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { EuiTitle, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Axis, Chart, getAxisId, niceTimeFormatter, Position, Settings } from '@elastic/charts'; +import { + Axis, + Chart, + getAxisId, + niceTimeFormatter, + Position, + Settings, + TooltipValue, +} from '@elastic/charts'; import { first, last } from 'lodash'; import moment from 'moment'; import { UICapabilities } from 'ui/capabilities'; @@ -27,6 +35,7 @@ import { SourceQuery } from '../../graphql/types'; import { MetricsExplorerEmptyChart } from './empty_chart'; import { MetricsExplorerNoMetrics } from './no_metrics'; import { getChartTheme } from './helpers/get_chart_theme'; +import { useKibanaUiSetting } from '../../utils/use_kibana_ui_setting'; import { calculateDomain } from './helpers/calculate_domain'; interface Props { @@ -60,6 +69,7 @@ export const MetricsExplorerChart = injectUICapabilities( uiCapabilities, }: Props) => { const { metrics } = options; + const [dateFormat] = useKibanaUiSetting('dateFormat'); const handleTimeChange = (from: number, to: number) => { onTimeChange(moment(from).toISOString(), moment(to).toISOString()); }; @@ -70,6 +80,12 @@ export const MetricsExplorerChart = injectUICapabilities( : (value: number) => `${value}`, [series.rows] ); + const tooltipProps = { + headerFormatter: useCallback( + (data: TooltipValue) => moment(data.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'), + [dateFormat] + ), + }; const yAxisFormater = useCallback(createFormatterForMetric(first(metrics)), [options]); const dataDomain = calculateDomain(series, metrics, chartOptions.stack); const domain = @@ -138,7 +154,11 @@ export const MetricsExplorerChart = injectUICapabilities( tickFormat={yAxisFormater} domain={domain} /> - + ) : options.metrics.length > 0 ? ( diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx index 9188bf7cadac..77be28f8ca6e 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx @@ -7,14 +7,14 @@ import { EuiComboBox } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; -import { StaticIndexPatternField } from 'ui/index_patterns'; +import { FieldType } from 'ui/index_patterns'; import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; interface Props { intl: InjectedIntl; options: MetricsExplorerOptions; onChange: (groupBy: string | null) => void; - fields: StaticIndexPatternField[]; + fields: FieldType[]; } export const MetricsExplorerGroupBy = injectI18n(({ intl, options, onChange, fields }: Props) => { diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx index cfe24b51113e..1ba512ef08ea 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx @@ -7,7 +7,7 @@ import { EuiComboBox } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { useCallback, useState, useEffect } from 'react'; -import { StaticIndexPatternField } from 'ui/index_patterns'; +import { FieldType } from 'ui/index_patterns'; import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; import { MetricsExplorerMetric, @@ -20,7 +20,7 @@ interface Props { autoFocus?: boolean; options: MetricsExplorerOptions; onChange: (metrics: MetricsExplorerMetric[]) => void; - fields: StaticIndexPatternField[]; + fields: FieldType[]; } interface SelectedOption { diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/series_chart.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/series_chart.tsx index b077d7c17a0f..74faf10b7a6d 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/series_chart.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/series_chart.tsx @@ -11,6 +11,8 @@ import { DataSeriesColorsValues, CustomSeriesColorsMap, AreaSeries, + RecursivePartial, + AreaSeriesStyle, } from '@elastic/charts'; import { MetricsExplorerSeries } from '../../../server/routes/metrics_explorer/types'; import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; @@ -43,26 +45,18 @@ export const MetricExplorerSeriesChart = ({ metric, id, series, type, stack }: P customColors.set(colors, color); const chartId = `series-${series.id}-${yAccessor}`; - const seriesAreaStyle = { + const seriesAreaStyle: RecursivePartial = { line: { - stroke: color, strokeWidth: 2, visible: true, }, area: { - fill: color, opacity: 0.5, visible: type === MetricsExplorerChartType.area, }, - border: { - visible: false, - strokeWidth: 2, - stroke: color, - }, point: { visible: false, radius: 0.2, - stroke: color, strokeWidth: 2, opacity: 1, }, diff --git a/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx b/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx new file mode 100644 index 000000000000..b1eef3400175 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx @@ -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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import euiStyled from '../../../../../common/eui_styled_components'; + +interface AppNavigationProps { + children: React.ReactNode; +} + +export const AppNavigation = ({ children }: AppNavigationProps) => ( + +); + +const Nav = euiStyled.nav` + background: ${props => props.theme.eui.euiColorEmptyShade}; + border-bottom: ${props => props.theme.eui.euiBorderThin}; + padding: ${props => + `${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL} ${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL}`} + + .euiTabs { + padding-left: 3px; + margin-left: -3px; + } +`; diff --git a/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx b/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx index 5c510fb2d3a6..5fe36d8c5af0 100644 --- a/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx +++ b/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTab, EuiTabs } from '@elastic/eui'; +import { EuiTab, EuiTabs, EuiLink } from '@elastic/eui'; import React from 'react'; import { Route } from 'react-router-dom'; +import euiStyled from '../../../../../common/eui_styled_components'; interface TabConfiguration { title: string; @@ -17,27 +18,42 @@ interface RoutedTabsProps { tabs: TabConfiguration[]; } +const noop = () => {}; + export class RoutedTabs extends React.Component { public render() { - return {this.renderTabs()}; + return {this.renderTabs()}; } private renderTabs() { return this.props.tabs.map(tab => { return ( ( - (match ? undefined : history.push(tab.path))} - isSelected={match !== null} - > - {tab.title} - + + { + e.preventDefault(); + history.push(tab.path); + }} + > + + {tab.title} + + + )} /> ); }); } } + +const TabContainer = euiStyled.div` + .euiLink { + color: inherit !important; + } +`; diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx index 5b79132ecf1f..771285e8ccee 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCode, EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiDescribedFormGroup, + EuiCode, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; @@ -39,145 +47,230 @@ export const FieldsConfigurationPanel = ({ - @timestamp, - }} + id="xpack.infra.sourceConfiguration.timestampFieldLabel" + defaultMessage="Timestamp" /> } - isInvalid={timestampFieldProps.isInvalid} - label={ + description={ } > - - - _doc, - }} + helpText={ + @timestamp, + }} + /> + } + isInvalid={timestampFieldProps.isInvalid} + label={ + + } + > + - } - isInvalid={tiebreakerFieldProps.isInvalid} - label={ + + + } - > - - - container.id, - }} + id="xpack.infra.sourceConfiguration.tiebreakerFieldDescription" + defaultMessage="Field used to break ties between two entries with the same timestamp" /> } - isInvalid={containerFieldProps.isInvalid} - label={ + > + _doc, + }} + /> + } + isInvalid={tiebreakerFieldProps.isInvalid} + label={ + + } + > + + + + } + description={ + + } > - - - container.id, + }} + /> + } + isInvalid={containerFieldProps.isInvalid} + label={ + + } + > + + + + host.name, - }} + id="xpack.infra.sourceConfiguration.hostNameFieldLabel" + defaultMessage="Host name" /> } - isInvalid={hostFieldProps.isInvalid} - label={ + description={ } > - - - kubernetes.pod.uid, - }} + helpText={ + host.name, + }} + /> + } + isInvalid={hostFieldProps.isInvalid} + label={ + + } + > + - } - isInvalid={podFieldProps.isInvalid} - label={ + + + } + description={ + + } > - - + helpText={ + kubernetes.pod.uid, + }} + /> + } + isInvalid={podFieldProps.isInvalid} + label={ + + } + > + + + ); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/index.ts b/x-pack/legacy/plugins/infra/public/components/source_configuration/index.ts index d1640f3e3470..4879a53ca329 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/index.ts +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/index.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SourceConfigurationButton } from './source_configuration_button'; -export { SourceConfigurationFlyout } from './source_configuration_flyout'; +export { SourceConfigurationSettings } from './source_configuration_settings'; export { - SourceConfigurationFlyoutState, - useSourceConfigurationFlyoutState, -} from './source_configuration_flyout_state'; + ViewSourceConfigurationButton, + ViewSourceConfigurationButtonHrefBase, +} from './view_source_configuration_button'; diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx index d2129345d71b..ee0e605baaf5 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCode, EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiCode, + EuiDescribedFormGroup, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; @@ -33,63 +41,97 @@ export const IndicesConfigurationPanel = ({ - metricbeat-*, - }} + id="xpack.infra.sourceConfiguration.metricIndicesTitle" + defaultMessage="Metric Indices" /> } - isInvalid={metricAliasFieldProps.isInvalid} - label={ + description={ } > - - - metricbeat-*, + }} + /> + } + isInvalid={metricAliasFieldProps.isInvalid} + label={ + + } + > + + + + filebeat-*, - }} + id="xpack.infra.sourceConfiguration.logIndicesTitle" + defaultMessage="Log Indices" /> } - isInvalid={logAliasFieldProps.isInvalid} - label={ + description={ } > - - + helpText={ + filebeat-*, + }} + /> + } + isInvalid={logAliasFieldProps.isInvalid} + label={ + + } + > + + + ); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx index bbc3cde41794..708fd34f2325 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx @@ -19,7 +19,8 @@ import { EuiDroppable, EuiIcon, } from '@elastic/eui'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; import { DragHandleProps, DropResult } from '../../../../../common/eui_draggable'; @@ -55,7 +56,7 @@ export const LogColumnsConfigurationPanel: React.FunctionComponent<

    @@ -167,29 +168,35 @@ const FieldLogColumnConfigurationPanel: React.FunctionComponent<{ remove, }, dragHandleProps, -}) => ( - - - -
    - -
    -
    - - - - - {field} - - - - -
    -
    -); +}) => { + const fieldLogColumnTitle = i18n.translate( + 'xpack.infra.sourceConfiguration.fieldLogColumnTitle', + { + defaultMessage: 'Field', + } + ); + return ( + + + +
    + +
    +
    + {fieldLogColumnTitle} + + {field} + + + + +
    +
    + ); +}; const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{ fieldName: React.ReactNode; @@ -213,31 +220,35 @@ const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{ - + ); -const RemoveLogColumnButton = injectI18n<{ +const RemoveLogColumnButton: React.FunctionComponent<{ onClick?: () => void; -}>(({ intl, onClick }) => { - const removeColumnLabel = intl.formatMessage({ - id: 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel', - defaultMessage: 'Remove this column', - }); + columnDescription: string; +}> = ({ onClick, columnDescription }) => { + const removeColumnLabel = i18n.translate( + 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel', + { + defaultMessage: 'Remove {columnDescription} column', + values: { columnDescription }, + } + ); return ( ); -}); +}; const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => ( - } + description={ + + } > - - + isInvalid={nameFieldProps.isInvalid} + label={ + + } + > + + + ); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_button.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_button.tsx deleted file mode 100644 index b05e5d9e34e0..000000000000 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_button.tsx +++ /dev/null @@ -1,31 +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 { EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useContext } from 'react'; - -import { SourceConfigurationFlyoutState } from './source_configuration_flyout_state'; - -export const SourceConfigurationButton: React.FunctionComponent = () => { - const { toggleIsVisible } = useContext(SourceConfigurationFlyoutState.Context); - - return ( - - - - ); -}; diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx deleted file mode 100644 index 4fea7b76e4f1..000000000000 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx +++ /dev/null @@ -1,281 +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 { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiTabbedContent, - EuiTabbedContentTab, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import React, { useCallback, useContext, useMemo } from 'react'; - -import { Source } from '../../containers/source'; -import { FieldsConfigurationPanel } from './fields_configuration_panel'; -import { IndicesConfigurationPanel } from './indices_configuration_panel'; -import { NameConfigurationPanel } from './name_configuration_panel'; -import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; -import { isValidTabId, SourceConfigurationFlyoutState } from './source_configuration_flyout_state'; -import { useSourceConfigurationFormState } from './source_configuration_form_state'; - -const noop = () => undefined; - -interface SourceConfigurationFlyoutProps { - intl: InjectedIntl; - shouldAllowEdit: boolean; -} - -export const SourceConfigurationFlyout = injectI18n( - ({ intl, shouldAllowEdit }: SourceConfigurationFlyoutProps) => { - const { activeTabId, hide, isVisible, setActiveTab } = useContext( - SourceConfigurationFlyoutState.Context - ); - - const { - createSourceConfiguration, - source, - sourceExists, - isLoading, - updateSourceConfiguration, - } = useContext(Source.Context); - const availableFields = useMemo( - () => (source && source.status ? source.status.indexFields.map(field => field.name) : []), - [source] - ); - - const { - addLogColumn, - moveLogColumn, - indicesConfigurationProps, - logColumnConfigurationProps, - errors, - resetForm, - isFormDirty, - isFormValid, - formState, - formStateChanges, - } = useSourceConfigurationFormState(source && source.configuration); - - const persistUpdates = useCallback(async () => { - if (sourceExists) { - await updateSourceConfiguration(formStateChanges); - } else { - await createSourceConfiguration(formState); - } - resetForm(); - }, [ - sourceExists, - updateSourceConfiguration, - createSourceConfiguration, - resetForm, - formState, - formStateChanges, - ]); - - const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [ - shouldAllowEdit, - source, - ]); - - const tabs: EuiTabbedContentTab[] = useMemo( - () => - isVisible - ? [ - { - id: 'indicesAndFieldsTab', - name: intl.formatMessage({ - id: 'xpack.infra.sourceConfiguration.sourceConfigurationIndicesTabTitle', - defaultMessage: 'Indices and fields', - }), - content: ( - <> - - - - - - - - ), - }, - { - id: 'logsTab', - name: intl.formatMessage({ - id: 'xpack.infra.sourceConfiguration.sourceConfigurationLogColumnsTabTitle', - defaultMessage: 'Log Columns', - }), - content: ( - <> - - - - ), - }, - ] - : [], - [ - addLogColumn, - moveLogColumn, - availableFields, - indicesConfigurationProps, - intl.formatMessage, - isLoading, - isVisible, - logColumnConfigurationProps, - isWriteable, - ] - ); - const activeTab = useMemo(() => tabs.filter(tab => tab.id === activeTabId)[0] || tabs[0], [ - activeTabId, - tabs, - ]); - const activateTab = useCallback( - (tab: EuiTabbedContentTab) => { - const tabId = tab.id; - if (isValidTabId(tabId)) { - setActiveTab(tabId); - } - }, - [setActiveTab, isValidTabId] - ); - - if (!isVisible || !source || !source.configuration) { - return null; - } - - return ( - - - -

    - {isWriteable ? ( - - ) : ( - - )} -

    -
    -
    - - - - - {errors.length > 0 ? ( - <> - -
      - {errors.map((error, errorIndex) => ( -
    • {error}
    • - ))} -
    -
    - - - ) : null} - - - {!isFormDirty ? ( - hide()} - > - - - ) : ( - { - resetForm(); - hide(); - }} - > - - - )} - - - {isWriteable && ( - - {isLoading ? ( - - Loading - - ) : ( - - - - )} - - )} - -
    -
    - ); - } -); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx deleted file mode 100644 index 6b12a4638d1e..000000000000 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx +++ /dev/null @@ -1,51 +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 createContainer from 'constate-latest'; -import { useCallback, useState } from 'react'; - -import { useVisibilityState } from '../../utils/use_visibility_state'; - -type TabId = 'indicesAndFieldsTab' | 'logsTab'; -const validTabIds: TabId[] = ['indicesAndFieldsTab', 'logsTab']; - -export const useSourceConfigurationFlyoutState = ({ - initialVisibility = false, - initialTab = 'indicesAndFieldsTab', -}: { - initialVisibility?: boolean; - initialTab?: TabId; -} = {}) => { - const { isVisible, show, hide, toggle: toggleIsVisible } = useVisibilityState(initialVisibility); - const [activeTabId, setActiveTab] = useState(initialTab); - - const showWithTab = useCallback( - (tabId?: TabId) => { - if (tabId != null) { - setActiveTab(tabId); - } - show(); - }, - [show] - ); - const showIndicesConfiguration = useCallback(() => showWithTab('indicesAndFieldsTab'), [show]); - const showLogsConfiguration = useCallback(() => showWithTab('logsTab'), [show]); - - return { - activeTabId, - hide, - isVisible, - setActiveTab, - show: showWithTab, - showIndicesConfiguration, - showLogsConfiguration, - toggleIsVisible, - }; -}; - -export const isValidTabId = (value: any): value is TabId => validTabIds.includes(value); - -export const SourceConfigurationFlyoutState = createContainer(useSourceConfigurationFlyoutState); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx new file mode 100644 index 000000000000..1afdeb887ae1 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; +import React, { useCallback, useContext, useMemo } from 'react'; +import { Prompt } from 'react-router-dom'; + +import { Source } from '../../containers/source'; +import { FieldsConfigurationPanel } from './fields_configuration_panel'; +import { IndicesConfigurationPanel } from './indices_configuration_panel'; +import { NameConfigurationPanel } from './name_configuration_panel'; +import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; +import { useSourceConfigurationFormState } from './source_configuration_form_state'; + +interface SourceConfigurationSettingsProps { + intl: InjectedIntl; + shouldAllowEdit: boolean; +} + +export const SourceConfigurationSettings = injectI18n( + ({ shouldAllowEdit }: SourceConfigurationSettingsProps) => { + const { + createSourceConfiguration, + source, + sourceExists, + isLoading, + updateSourceConfiguration, + } = useContext(Source.Context); + + const availableFields = useMemo( + () => (source && source.status ? source.status.indexFields.map(field => field.name) : []), + [source] + ); + + const { + addLogColumn, + moveLogColumn, + indicesConfigurationProps, + logColumnConfigurationProps, + errors, + resetForm, + isFormDirty, + isFormValid, + formState, + formStateChanges, + } = useSourceConfigurationFormState(source && source.configuration); + + const persistUpdates = useCallback(async () => { + if (sourceExists) { + await updateSourceConfiguration(formStateChanges); + } else { + await createSourceConfiguration(formState); + } + resetForm(); + }, [ + sourceExists, + updateSourceConfiguration, + createSourceConfiguration, + resetForm, + formState, + formStateChanges, + ]); + + const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [ + shouldAllowEdit, + source, + ]); + + if (!source || !source.configuration) { + return null; + } + + return ( + <> + + + + + + + + + + + + + + + + + + + + + {errors.length > 0 ? ( + <> + +
      + {errors.map((error, errorIndex) => ( +
    • {error}
    • + ))} +
    +
    + + + ) : null} + + + {isWriteable && ( + + {isLoading ? ( + + + + Loading + + + + ) : ( + <> + + + { + resetForm(); + }} + > + + + + + + + + + + + )} + + )} + +
    +
    +
    +
    + + ); + } +); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx new file mode 100644 index 000000000000..9b584b2ef3bd --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx @@ -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 { EuiButton } from '@elastic/eui'; +import React from 'react'; +import { Route } from 'react-router-dom'; + +export enum ViewSourceConfigurationButtonHrefBase { + infrastructure = 'infrastructure', + logs = 'logs', +} + +interface ViewSourceConfigurationButtonProps { + 'data-test-subj'?: string; + hrefBase: ViewSourceConfigurationButtonHrefBase; + children: React.ReactNode; +} + +export const ViewSourceConfigurationButton = ({ + 'data-test-subj': dataTestSubj, + hrefBase, + children, +}: ViewSourceConfigurationButtonProps) => { + const href = `/${hrefBase}/settings`; + + return ( + ( + history.push(href)}> + {children} + + )} + /> + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/conditional_tooltip.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/conditional_tooltip.tsx new file mode 100644 index 000000000000..eda74da708c8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/waffle/conditional_tooltip.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 { EuiToolTip, EuiToolTipProps } from '@elastic/eui'; +import { omit } from 'lodash'; + +interface Props extends EuiToolTipProps { + hidden: boolean; +} + +export const ConditionalToolTip = (props: Props) => { + if (props.hidden) { + return props.children; + } + const propsWithoutHidden = omit(props, 'hidden'); + return {props.children}; +}; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx index 58444ab5e7d2..04748095b7b9 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx @@ -7,10 +7,10 @@ import { EuiButton, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; -import { InfraIndexField } from '../../../server/graphql/types'; +import { FieldType } from 'ui/index_patterns'; interface Props { onSubmit: (field: string) => void; - fields: InfraIndexField[]; + fields: FieldType[]; intl: InjectedIntl; } diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx index d10c8b622377..b3845c570a9c 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiToolTip } from '@elastic/eui'; import moment from 'moment'; import { darken, readableColor } from 'polished'; import React from 'react'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { ConditionalToolTip } from './conditional_tooltip'; import euiStyled from '../../../../../common/eui_styled_components'; import { InfraTimerangeInput, InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapBounds, InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; @@ -75,9 +75,14 @@ export const Node = injectI18n( closePopover={this.closePopover} options={options} timeRange={newTimerange} - popoverPosition="upCenter" + popoverPosition="downCenter" > - + + ); } diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx index 24eacf840fd6..e39099899c41 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx @@ -15,7 +15,8 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; -import { InfraIndexField, InfraNodeType, InfraSnapshotGroupbyInput } from '../../graphql/types'; +import { FieldType } from 'ui/index_patterns'; +import { InfraNodeType, InfraSnapshotGroupbyInput } from '../../graphql/types'; import { InfraGroupByOptions } from '../../lib/lib'; import { CustomFieldPanel } from './custom_field_panel'; import { fieldToName } from './lib/field_to_display_name'; @@ -26,7 +27,7 @@ interface Props { groupBy: InfraSnapshotGroupbyInput[]; onChange: (groupBy: InfraSnapshotGroupbyInput[]) => void; onChangeCustomOptions: (options: InfraGroupByOptions[]) => void; - fields: InfraIndexField[]; + fields: FieldType[]; intl: InjectedIntl; customOptions: InfraGroupByOptions[]; } diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts new file mode 100644 index 000000000000..784c02f89615 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/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 './log_analysis_results'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx new file mode 100644 index 000000000000..190344103782 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.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 createContainer from 'constate-latest/dist/ts/src'; +import { useMemo } from 'react'; + +import { useLogEntryRate } from './log_entry_rate'; + +export const useLogAnalysisResults = ({ sourceId }: { sourceId: string }) => { + const { isLoading: isLoadingLogEntryRate, logEntryRate } = useLogEntryRate({ sourceId }); + + const isLoading = useMemo(() => isLoadingLogEntryRate, [isLoadingLogEntryRate]); + + return { + isLoading, + logEntryRate, + }; +}; + +export const LogAnalysisResults = createContainer(useLogAnalysisResults); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx new file mode 100644 index 000000000000..aee953f6b0f3 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.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 { useMemo, useState } from 'react'; +import { kfetch } from 'ui/kfetch'; + +import { + getLogEntryRateRequestPayloadRT, + getLogEntryRateSuccessReponsePayloadRT, + GetLogEntryRateSuccessResponsePayload, + LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, +} from '../../../../common/http_api/log_analysis'; +import { createPlainError, throwErrors } from '../../../../common/runtime_types'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; + +type LogEntryRateResults = GetLogEntryRateSuccessResponsePayload['data']; + +export const useLogEntryRate = ({ sourceId }: { sourceId: string }) => { + const [logEntryRate, setLogEntryRate] = useState(null); + + const [getLogEntryRateRequest, getLogEntryRate] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await kfetch({ + method: 'POST', + pathname: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, + body: JSON.stringify( + getLogEntryRateRequestPayloadRT.encode({ + data: { + sourceId, // TODO: get from hook arguments + timeRange: { + startTime: Date.now(), // TODO: get from hook arguments + endTime: Date.now() + 1000 * 60 * 60, // TODO: get from hook arguments + }, + bucketDuration: 15 * 60 * 1000, // TODO: get from hook arguments + }, + }) + ), + }); + }, + onResolve: response => { + const { data } = getLogEntryRateSuccessReponsePayloadRT + .decode(response) + .getOrElseL(throwErrors(createPlainError)); + + setLogEntryRate(data); + }, + }, + [sourceId] + ); + + const isLoading = useMemo(() => getLogEntryRateRequest.state === 'pending', [ + getLogEntryRateRequest.state, + ]); + + return { + getLogEntryRate, + isLoading, + logEntryRate, + }; +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/metadata/lib/get_filtered_layouts.ts b/x-pack/legacy/plugins/infra/public/containers/metadata/lib/get_filtered_layouts.ts new file mode 100644 index 000000000000..a543365aa68d --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/metadata/lib/get_filtered_layouts.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 { InfraMetadataFeature } from '../../../../common/http_api/metadata_api'; +import { InfraMetricLayout } from '../../../pages/metrics/layouts/types'; + +export const getFilteredLayouts = ( + layouts: InfraMetricLayout[], + metadata: Array | undefined +): InfraMetricLayout[] => { + if (!metadata) { + return layouts; + } + + const metricMetadata: Array = metadata + .filter(data => data && data.source === 'metrics') + .map(data => data && data.name); + + // After filtering out sections that can't be displayed, a layout may end up empty and can be removed. + const filteredLayouts = layouts + .map(layout => getFilteredLayout(layout, metricMetadata)) + .filter(layout => layout.sections.length > 0); + return filteredLayouts; +}; + +export const getFilteredLayout = ( + layout: InfraMetricLayout, + metricMetadata: Array +): InfraMetricLayout => { + // A section is only displayed if at least one of its requirements is met + // All others are filtered out. + const filteredSections = layout.sections.filter( + section => _.intersection(section.requires, metricMetadata).length > 0 + ); + return { ...layout, sections: filteredSections }; +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/metadata/metadata.gql_query.ts b/x-pack/legacy/plugins/infra/public/containers/metadata/metadata.gql_query.ts deleted file mode 100644 index 9a59cfcbee9e..000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/metadata/metadata.gql_query.ts +++ /dev/null @@ -1,22 +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 gql from 'graphql-tag'; - -export const metadataQuery = gql` - query MetadataQuery($sourceId: ID!, $nodeId: String!, $nodeType: InfraNodeType!) { - source(id: $sourceId) { - id - metadataByNode(nodeId: $nodeId, nodeType: $nodeType) { - name - features { - name - source - } - } - } - } -`; diff --git a/x-pack/legacy/plugins/infra/public/containers/metadata/use_metadata.ts b/x-pack/legacy/plugins/infra/public/containers/metadata/use_metadata.ts new file mode 100644 index 000000000000..941c7532d5b2 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/metadata/use_metadata.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 } from 'react'; +import { InfraNodeType } from '../../graphql/types'; +import { InfraMetricLayout } from '../../pages/metrics/layouts/types'; +import { InfraMetadata, InfraMetadataRT } from '../../../common/http_api/metadata_api'; +import { getFilteredLayouts } from './lib/get_filtered_layouts'; +import { useHTTPRequest } from '../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../common/runtime_types'; + +export function useMetadata( + nodeId: string, + nodeType: InfraNodeType, + layouts: InfraMetricLayout[], + sourceId: string +) { + const decodeResponse = (response: any) => { + return InfraMetadataRT.decode(response).getOrElseL(throwErrors(createPlainError)); + }; + + const { error, loading, response, makeRequest } = useHTTPRequest( + '/api/infra/metadata', + 'POST', + JSON.stringify({ + nodeId, + nodeType, + sourceId, + decodeResponse, + }) + ); + + useEffect(() => { + (async () => { + await makeRequest(); + })(); + }, [makeRequest]); + + return { + name: (response && response.name) || '', + filteredLayouts: (response && getFilteredLayouts(layouts, response.features)) || [], + error: (error && error.message) || null, + loading, + }; +} diff --git a/x-pack/legacy/plugins/infra/public/containers/metadata/with_metadata.tsx b/x-pack/legacy/plugins/infra/public/containers/metadata/with_metadata.tsx deleted file mode 100644 index 1950ecf43653..000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/metadata/with_metadata.tsx +++ /dev/null @@ -1,90 +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 _ from 'lodash'; -import React from 'react'; -import { Query } from 'react-apollo'; - -import { InfraNodeType, MetadataQuery } from '../../graphql/types'; -import { InfraMetricLayout } from '../../pages/metrics/layouts/types'; -import { metadataQuery } from './metadata.gql_query'; - -interface WithMetadataProps { - children: (args: WithMetadataArgs) => React.ReactNode; - layouts: InfraMetricLayout[]; - nodeType: InfraNodeType; - nodeId: string; - sourceId: string; -} - -interface WithMetadataArgs { - name: string; - filteredLayouts: InfraMetricLayout[]; - error?: string | undefined; - loading: boolean; -} - -export const WithMetadata = ({ - children, - layouts, - nodeType, - nodeId, - sourceId, -}: WithMetadataProps) => { - return ( - - query={metadataQuery} - fetchPolicy="no-cache" - variables={{ - sourceId, - nodeType, - nodeId, - }} - > - {({ data, error, loading }) => { - const metadata = data && data.source && data.source.metadataByNode; - const filteredLayouts = (metadata && getFilteredLayouts(layouts, metadata.features)) || []; - return children({ - name: (metadata && metadata.name) || '', - filteredLayouts, - error: error && error.message, - loading, - }); - }} - - ); -}; - -const getFilteredLayouts = ( - layouts: InfraMetricLayout[], - metadata: Array | undefined -): InfraMetricLayout[] => { - if (!metadata) { - return layouts; - } - - const metricMetadata: Array = metadata - .filter(data => data && data.source === 'metrics') - .map(data => data && data.name); - - // After filtering out sections that can't be displayed, a layout may end up empty and can be removed. - const filteredLayouts = layouts - .map(layout => getFilteredLayout(layout, metricMetadata)) - .filter(layout => layout.sections.length > 0); - return filteredLayouts; -}; - -const getFilteredLayout = ( - layout: InfraMetricLayout, - metricMetadata: Array -): InfraMetricLayout => { - // A section is only displayed if at least one of its requirements is met - // All others are filtered out. - const filteredSections = layout.sections.filter( - section => _.intersection(section.requires, metricMetadata).length > 0 - ); - return { ...layout, sections: filteredSections }; -}; diff --git a/x-pack/legacy/plugins/infra/public/containers/source/source.tsx b/x-pack/legacy/plugins/infra/public/containers/source/source.tsx index 9632a5bda079..8b931545a5c6 100644 --- a/x-pack/legacy/plugins/infra/public/containers/source/source.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/source/source.tsx @@ -21,6 +21,19 @@ import { updateSourceMutation } from './update_source.gql_query'; type Source = SourceQuery.Query['source']; +const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => { + if (!source) { + return 'unknown-index'; + } + if (type === 'logs') { + return source.configuration.logAlias; + } + if (type === 'metrics') { + return source.configuration.metricAlias; + } + return `${source.configuration.logAlias},${source.configuration.metricAlias}`; +}; + export const useSource = ({ sourceId }: { sourceId: string }) => { const apolloClient = useApolloClient(); const [source, setSource] = useState(undefined); @@ -108,13 +121,12 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { [apolloClient, sourceId] ); - const derivedIndexPattern = useMemo( - () => ({ + const createDerivedIndexPattern = (type: 'logs' | 'metrics' | 'both') => { + return { fields: source ? source.status.indexFields : [], - title: source ? `${source.configuration.logAlias}` : 'unknown-index', - }), - [source] - ); + title: pickIndexPattern(source, type), + }; + }; const isLoading = useMemo( () => @@ -146,7 +158,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { return { createSourceConfiguration, - derivedIndexPattern, + createDerivedIndexPattern, logIndicesExist, isLoading, isLoadingSource: loadSourceRequest.state === 'pending', diff --git a/x-pack/legacy/plugins/infra/public/containers/with_source/with_source.tsx b/x-pack/legacy/plugins/infra/public/containers/with_source/with_source.tsx index 3ed3852484e7..0512888ecd4e 100644 --- a/x-pack/legacy/plugins/infra/public/containers/with_source/with_source.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/with_source/with_source.tsx @@ -15,7 +15,7 @@ interface WithSourceProps { children: RendererFunction<{ configuration?: SourceQuery.Query['source']['configuration']; create: (sourceProperties: UpdateSourceInput) => Promise | undefined; - derivedIndexPattern: StaticIndexPattern; + createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => StaticIndexPattern; exists?: boolean; hasFailed: boolean; isLoading: boolean; @@ -33,7 +33,7 @@ interface WithSourceProps { export const WithSource: React.FunctionComponent = ({ children }) => { const { createSourceConfiguration, - derivedIndexPattern, + createDerivedIndexPattern, source, sourceExists, sourceId, @@ -50,7 +50,7 @@ export const WithSource: React.FunctionComponent = ({ children return children({ create: createSourceConfiguration, configuration: source && source.configuration, - derivedIndexPattern, + createDerivedIndexPattern, exists: sourceExists, hasFailed: hasFailedLoadingSource, isLoading, diff --git a/x-pack/legacy/plugins/infra/public/graphql/introspection.json b/x-pack/legacy/plugins/infra/public/graphql/introspection.json index c937d4f365e6..055fac61cb93 100644 --- a/x-pack/legacy/plugins/infra/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/infra/public/graphql/introspection.json @@ -137,39 +137,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "metadataByNode", - "description": "A hierarchy of metadata entries by node", - "args": [ - { - "name": "nodeId", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "nodeType", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "InfraNodeType", "ofType": null } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "InfraNodeMetadata", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "logEntriesAround", "description": "A consecutive span of log entries surrounding a point in time", @@ -1086,115 +1053,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "ENUM", - "name": "InfraNodeType", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { "name": "pod", "description": "", "isDeprecated": false, "deprecationReason": null }, - { - "name": "container", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "host", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "InfraNodeMetadata", - "description": "One metadata entry for a node.", - "fields": [ - { - "name": "id", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "features", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "InfraNodeFeature", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "InfraNodeFeature", - "description": "", - "fields": [ - { - "name": "name", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "source", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "INPUT_OBJECT", "name": "InfraTimeKeyInput", @@ -2039,6 +1897,25 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "InfraNodeType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "pod", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "container", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "host", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "InfraSnapshotGroupbyInput", diff --git a/x-pack/legacy/plugins/infra/public/graphql/types.ts b/x-pack/legacy/plugins/infra/public/graphql/types.ts index 2da829dbf293..7843a54b93be 100644 --- a/x-pack/legacy/plugins/infra/public/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/public/graphql/types.ts @@ -28,8 +28,6 @@ export interface InfraSource { configuration: InfraSourceConfiguration; /** The status of the source */ status: InfraSourceStatus; - /** A hierarchy of metadata entries by node */ - metadataByNode: InfraNodeMetadata; /** A consecutive span of log entries surrounding a point in time */ logEntriesAround: InfraLogEntryInterval; /** A consecutive span of log entries within an interval */ @@ -132,20 +130,6 @@ export interface InfraIndexField { /** Whether the field's values can be aggregated */ aggregatable: boolean; } -/** One metadata entry for a node. */ -export interface InfraNodeMetadata { - id: string; - - name: string; - - features: InfraNodeFeature[]; -} - -export interface InfraNodeFeature { - name: string; - - source: string; -} /** A consecutive sequence of log entries */ export interface InfraLogEntryInterval { /** The key corresponding to the start of the interval covered by the entries */ @@ -424,11 +408,6 @@ export interface SourceQueryArgs { /** The id of the source */ id: string; } -export interface MetadataByNodeInfraSourceArgs { - nodeId: string; - - nodeType: InfraNodeType; -} export interface LogEntriesAroundInfraSourceArgs { /** The sort key that corresponds to the point in time */ key: InfraTimeKeyInput; @@ -722,44 +701,6 @@ export namespace LogSummary { }; } -export namespace MetadataQuery { - export type Variables = { - sourceId: string; - nodeId: string; - nodeType: InfraNodeType; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'InfraSource'; - - id: string; - - metadataByNode: MetadataByNode; - }; - - export type MetadataByNode = { - __typename?: 'InfraNodeMetadata'; - - name: string; - - features: Features[]; - }; - - export type Features = { - __typename?: 'InfraNodeFeature'; - - name: string; - - source: string; - }; -} - export namespace MetricsQuery { export type Variables = { sourceId: string; diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx new file mode 100644 index 000000000000..606f7d0aecdc --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx @@ -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 React, { useMemo, useState } from 'react'; +import { kfetch } from 'ui/kfetch'; +import { toastNotifications } from 'ui/notify'; +import { i18n } from '@kbn/i18n'; +import { idx } from '@kbn/elastic-idx/target'; +import { KFetchError } from 'ui/kfetch/kfetch_error'; +import { useTrackedPromise } from '../utils/use_tracked_promise'; +export function useHTTPRequest( + pathname: string, + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD', + body?: string, + decode: (response: any) => Response = response => response +) { + const [response, setResponse] = useState(null); + const [error, setError] = useState(null); + const [request, makeRequest] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: () => + kfetch({ + method, + pathname, + body, + }), + onResolve: resp => setResponse(decode(resp)), + onReject: (e: unknown) => { + const err = e as KFetchError; + setError(err); + toastNotifications.addWarning({ + title: i18n.translate('xpack.infra.useHTTPRequest.error.title', { + defaultMessage: `Error while fetching resource`, + }), + text: ( +
    +
    + {i18n.translate('xpack.infra.useHTTPRequest.error.status', { + defaultMessage: `Error`, + })} +
    + {idx(err.res, r => r.statusText)} ({idx(err.res, r => r.status)}) +
    + {i18n.translate('xpack.infra.useHTTPRequest.error.url', { + defaultMessage: `URL`, + })} +
    + {idx(err.res, r => r.url)} +
    + ), + }); + }, + }, + [pathname, body, method] + ); + + const loading = useMemo(() => request.state === 'pending', [request.state]); + + return { + response, + error, + loading, + makeRequest, + }; +} diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx index 11b5799fe728..bc858a82da69 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx @@ -7,26 +7,31 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { RoutedTabs } from '../../components/navigation/routed_tabs'; import { ColumnarPage } from '../../components/page'; +import { Header } from '../../components/header'; import { MetricsExplorerOptionsContainer } from '../../containers/metrics_explorer/use_metrics_explorer_options'; import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; import { WithSource } from '../../containers/with_source'; -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; import { Source } from '../../containers/source'; import { MetricsExplorerPage } from './metrics_explorer'; import { SnapshotPage } from './snapshot'; +import { SettingsPage } from '../shared/settings'; +import { AppNavigation } from '../../components/navigation/app_navigation'; interface InfrastructurePageProps extends RouteComponentProps { intl: InjectedIntl; + uiCapabilities: UICapabilities; } -export const InfrastructurePage = injectI18n(({ match, intl }: InfrastructurePageProps) => ( - - +export const InfrastructurePage = injectUICapabilities( + injectI18n(({ match, intl, uiCapabilities }: InfrastructurePageProps) => ( + - + + + + ( - {({ configuration, derivedIndexPattern }) => ( + {({ configuration, createDerivedIndexPattern }) => ( @@ -81,8 +107,9 @@ export const InfrastructurePage = injectI18n(({ match, intl }: InfrastructurePag )} /> + - - -)); + + )) +); diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx index 5c28715e4e53..c4a37af08b81 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx @@ -14,13 +14,14 @@ import { SnapshotToolbar } from './toolbar'; import { DocumentTitle } from '../../../components/document_title'; import { NoIndices } from '../../../components/empty_states/no_indices'; -import { Header } from '../../../components/header'; import { ColumnarPage } from '../../../components/page'; -import { SourceConfigurationFlyout } from '../../../components/source_configuration'; -import { SourceConfigurationFlyoutState } from '../../../components/source_configuration'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { + ViewSourceConfigurationButton, + ViewSourceConfigurationButtonHrefBase, +} from '../../../components/source_configuration'; import { Source } from '../../../containers/source'; import { WithWaffleFilterUrlState } from '../../../containers/waffle/with_waffle_filters'; import { WithWaffleOptionsUrlState } from '../../../containers/waffle/with_waffle_options'; @@ -36,9 +37,8 @@ interface SnapshotPageProps { export const SnapshotPage = injectUICapabilities( injectI18n((props: SnapshotPageProps) => { const { intl, uiCapabilities } = props; - const { showIndicesConfiguration } = useContext(SourceConfigurationFlyoutState.Context); const { - derivedIndexPattern, + createDerivedIndexPattern, hasFailedLoadingSource, isLoading, loadSourceFailureMessage, @@ -64,27 +64,12 @@ export const SnapshotPage = injectUICapabilities( ) } /> -
    - {isLoading ? ( ) : metricIndicesExist ? ( <> - + @@ -120,16 +105,15 @@ export const SnapshotPage = injectUICapabilities( {uiCapabilities.infrastructure.configureSource ? ( - {intl.formatMessage({ id: 'xpack.infra.configureSourceActionLabel', defaultMessage: 'Change source configuration', })} - + ) : null} diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx index a47b82d17d77..f6c9bb27e36c 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx @@ -19,10 +19,10 @@ import { WithSource } from '../../../containers/with_source'; export const SnapshotPageContent: React.SFC = () => ( - {({ configuration, derivedIndexPattern, sourceId }) => ( + {({ configuration, createDerivedIndexPattern, sourceId }) => ( {({ wafflemap }) => ( - + {({ filterQueryAsJson, applyFilterQuery }) => ( {({ currentTimeRange, isAutoReloading }) => ( diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx index e70e79a18a66..5387d4901d24 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { AutocompleteField } from '../../../components/autocomplete_field'; import { Toolbar } from '../../../components/eui/toolbar'; -import { SourceConfigurationButton } from '../../../components/source_configuration'; import { WaffleGroupByControls } from '../../../components/waffle/waffle_group_by_controls'; import { WaffleMetricControls } from '../../../components/waffle/waffle_metric_controls'; import { WaffleNodeTypeSwitcher } from '../../../components/waffle/waffle_node_type_switcher'; @@ -26,10 +25,10 @@ export const SnapshotToolbar = injectI18n(({ intl }) => ( - {({ derivedIndexPattern }) => ( - + {({ createDerivedIndexPattern }) => ( + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( - + {({ applyFilterQueryFromKueryExpression, filterQueryDraft, @@ -73,7 +72,7 @@ export const SnapshotToolbar = injectI18n(({ intl }) => ( - {({ derivedIndexPattern }) => ( + {({ createDerivedIndexPattern }) => ( {({ changeMetric, @@ -106,14 +105,11 @@ export const SnapshotToolbar = injectI18n(({ intl }) => ( groupBy={groupBy} nodeType={nodeType} onChange={changeGroupBy} - fields={derivedIndexPattern.fields} + fields={createDerivedIndexPattern('metrics').fields} onChangeCustomOptions={changeCustomOptions} customOptions={customOptions} /> - - - )} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/index.ts deleted file mode 100644 index 1d1c8cc65287..000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { LogsPage } from './page'; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx new file mode 100644 index 000000000000..dbcd046549c9 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx @@ -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 { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; + +import { DocumentTitle } from '../../components/document_title'; +import { HelpCenterContent } from '../../components/help_center_content'; +import { Header } from '../../components/header'; +import { RoutedTabs } from '../../components/navigation/routed_tabs'; +import { ColumnarPage } from '../../components/page'; +import { Source } from '../../containers/source'; +import { StreamPage } from './stream'; +import { SettingsPage } from '../shared/settings'; +import { AppNavigation } from '../../components/navigation/app_navigation'; + +interface LogsPageProps extends RouteComponentProps { + intl: InjectedIntl; + uiCapabilities: UICapabilities; +} + +export const LogsPage = injectUICapabilities( + injectI18n(({ match, intl, uiCapabilities }: LogsPageProps) => ( + + + + + + +
    + + + + + + + + + + + + )) +); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/page.tsx deleted file mode 100644 index 32c86641d675..000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page.tsx +++ /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 React from 'react'; - -import { ColumnarPage } from '../../components/page'; -import { LogsPageContent } from './page_content'; -import { LogsPageHeader } from './page_header'; -import { LogsPageProviders } from './page_providers'; -import { useTrackPageview } from '../../hooks/use_track_metric'; - -export const LogsPage = () => { - useTrackPageview({ app: 'infra_logs', path: 'stream' }); - useTrackPageview({ app: 'infra_logs', path: 'stream', delay: 15000 }); - return ( - - - - - - - ); -}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_header.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/page_header.tsx deleted file mode 100644 index d42a13b0e4de..000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_header.tsx +++ /dev/null @@ -1,57 +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 { injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import React from 'react'; - -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; -import { DocumentTitle } from '../../components/document_title'; -import { Header } from '../../components/header'; -import { HelpCenterContent } from '../../components/help_center_content'; -import { SourceConfigurationFlyout } from '../../components/source_configuration'; - -interface LogsPageHeaderProps { - intl: InjectedIntl; - uiCapabilities: UICapabilities; -} - -export const LogsPageHeader = injectUICapabilities( - injectI18n((props: LogsPageHeaderProps) => { - const { intl, uiCapabilities } = props; - return ( - <> -
    - - - - - ); - }) -); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/page_providers.tsx deleted file mode 100644 index 82aea7b38003..000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_providers.tsx +++ /dev/null @@ -1,33 +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 { SourceConfigurationFlyoutState } from '../../components/source_configuration'; -import { LogFlyout } from '../../containers/logs/log_flyout'; -import { LogViewConfiguration } from '../../containers/logs/log_view_configuration'; -import { LogHighlightsState } from '../../containers/logs/log_highlights/log_highlights'; -import { Source, useSource } from '../../containers/source'; -import { useSourceId } from '../../containers/source_id'; - -export const LogsPageProviders: React.FunctionComponent = ({ children }) => { - const [sourceId] = useSourceId(); - const source = useSource({ sourceId }); - - return ( - - - - - - {children} - - - - - - ); -}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_toolbar.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/page_toolbar.tsx deleted file mode 100644 index 2255cbff3d0c..000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_toolbar.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; -import React, { useContext } from 'react'; - -import { AutocompleteField } from '../../components/autocomplete_field'; -import { Toolbar } from '../../components/eui'; -import { LogCustomizationMenu } from '../../components/logging/log_customization_menu'; -import { LogHighlightsMenu } from '../../components/logging/log_highlights_menu'; -import { LogHighlightsState } from '../../containers/logs/log_highlights/log_highlights'; -import { LogMinimapScaleControls } from '../../components/logging/log_minimap_scale_controls'; -import { LogTextScaleControls } from '../../components/logging/log_text_scale_controls'; -import { LogTextWrapControls } from '../../components/logging/log_text_wrap_controls'; -import { LogTimeControls } from '../../components/logging/log_time_controls'; -import { SourceConfigurationButton } from '../../components/source_configuration'; -import { LogFlyout } from '../../containers/logs/log_flyout'; -import { LogViewConfiguration } from '../../containers/logs/log_view_configuration'; -import { WithLogFilter } from '../../containers/logs/with_log_filter'; -import { WithLogPosition } from '../../containers/logs/with_log_position'; -import { Source } from '../../containers/source'; -import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; - -export const LogsToolbar = injectI18n(({ intl }) => { - const { derivedIndexPattern } = useContext(Source.Context); - const { - availableIntervalSizes, - availableTextScales, - intervalSize, - setIntervalSize, - setTextScale, - setTextWrap, - textScale, - textWrap, - } = useContext(LogViewConfiguration.Context); - - const { setSurroundingLogsId } = useContext(LogFlyout.Context); - - const { - setHighlightTerms, - loadLogEntryHighlightsRequest, - highlightTerms, - hasPreviousHighlight, - hasNextHighlight, - goToPreviousHighlight, - goToNextHighlight, - } = useContext(LogHighlightsState.Context); - return ( - - - - - {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( - - {({ - applyFilterQueryFromKueryExpression, - filterQueryDraft, - isFilterQueryDraftValid, - setFilterQueryDraftFromKueryExpression, - }) => ( - { - setSurroundingLogsId(null); - setFilterQueryDraftFromKueryExpression(expression); - }} - onSubmit={(expression: string) => { - setSurroundingLogsId(null); - applyFilterQueryFromKueryExpression(expression); - }} - placeholder={intl.formatMessage({ - id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder', - defaultMessage: 'Search for log entries… (e.g. host.name:host-1)', - })} - suggestions={suggestions} - value={filterQueryDraft ? filterQueryDraft.expression : ''} - /> - )} - - )} - - - - - - - - - - - - - - highlightTerm.length > 0).length > 0 - } - goToPreviousHighlight={goToPreviousHighlight} - goToNextHighlight={goToNextHighlight} - hasPreviousHighlight={hasPreviousHighlight} - hasNextHighlight={hasNextHighlight} - /> - - - - {({ - visibleMidpointTime, - isAutoReloading, - jumpToTargetPositionTime, - startLiveStreaming, - stopLiveStreaming, - }) => ( - { - startLiveStreaming(interval); - setSurroundingLogsId(null); - }} - stopLiveStreaming={stopLiveStreaming} - /> - )} - - - - - ); -}); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/stream/index.ts new file mode 100644 index 000000000000..928c6187ec07 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/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 { StreamPage } from './page'; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page.tsx new file mode 100644 index 000000000000..9031a8cb4785 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page.tsx @@ -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 React from 'react'; + +import { ColumnarPage } from '../../../components/page'; +import { StreamPageContent } from './page_content'; +import { StreamPageHeader } from './page_header'; +import { LogsPageProviders } from './page_providers'; +import { useTrackPageview } from '../../../hooks/use_track_metric'; + +export const StreamPage = () => { + useTrackPageview({ app: 'infra_logs', path: 'stream' }); + useTrackPageview({ app: 'infra_logs', path: 'stream', delay: 15000 }); + return ( + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx similarity index 77% rename from x-pack/legacy/plugins/infra/public/pages/logs/page_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx index a8dde0532283..abcc881dd250 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -6,13 +6,13 @@ import React, { useContext } from 'react'; -import { SourceErrorPage } from '../../components/source_error_page'; -import { SourceLoadingPage } from '../../components/source_loading_page'; -import { Source } from '../../containers/source'; +import { SourceErrorPage } from '../../../components/source_error_page'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { Source } from '../../../containers/source'; import { LogsPageLogsContent } from './page_logs_content'; import { LogsPageNoIndicesContent } from './page_no_indices_content'; -export const LogsPageContent: React.FunctionComponent = () => { +export const StreamPageContent: React.FunctionComponent = () => { const { hasFailedLoadingSource, isLoadingSource, diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_header.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_header.tsx new file mode 100644 index 000000000000..9567784e314d --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_header.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 { i18n } from '@kbn/i18n'; +import React from 'react'; + +import { DocumentTitle } from '../../../components/document_title'; + +export const StreamPageHeader = () => { + return ( + <> + + i18n.translate('xpack.infra.logs.streamPage.documentTitle', { + defaultMessage: '{previousTitle} | Stream', + values: { + previousTitle, + }, + }) + } + /> + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_logs_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx similarity index 75% rename from x-pack/legacy/plugins/infra/public/pages/logs/page_logs_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index 7f4b52604e46..2ead89f94f81 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_logs_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -6,33 +6,32 @@ import React, { useContext } from 'react'; -import euiStyled from '../../../../../common/eui_styled_components'; -import { AutoSizer } from '../../components/auto_sizer'; -import { LogEntryFlyout } from '../../components/logging/log_entry_flyout'; -import { LogMinimap } from '../../components/logging/log_minimap'; -import { ScrollableLogTextStreamView } from '../../components/logging/log_text_stream'; -import { PageContent } from '../../components/page'; +import euiStyled from '../../../../../../common/eui_styled_components'; +import { AutoSizer } from '../../../components/auto_sizer'; +import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout'; +import { LogMinimap } from '../../../components/logging/log_minimap'; +import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream'; +import { PageContent } from '../../../components/page'; -import { WithSummary } from '../../containers/logs/log_summary'; -import { LogViewConfiguration } from '../../containers/logs/log_view_configuration'; -import { WithLogFilter, WithLogFilterUrlState } from '../../containers/logs/with_log_filter'; +import { WithSummary } from '../../../containers/logs/log_summary'; +import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; +import { WithLogFilter, WithLogFilterUrlState } from '../../../containers/logs/with_log_filter'; import { LogFlyout as LogFlyoutState, WithFlyoutOptionsUrlState, -} from '../../containers/logs/log_flyout'; -import { WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap'; -import { WithLogPositionUrlState } from '../../containers/logs/with_log_position'; -import { WithLogPosition } from '../../containers/logs/with_log_position'; -import { WithLogTextviewUrlState } from '../../containers/logs/with_log_textview'; -import { ReduxSourceIdBridge, WithStreamItems } from '../../containers/logs/with_stream_items'; -import { Source } from '../../containers/source'; +} from '../../../containers/logs/log_flyout'; +import { WithLogMinimapUrlState } from '../../../containers/logs/with_log_minimap'; +import { WithLogPositionUrlState } from '../../../containers/logs/with_log_position'; +import { WithLogPosition } from '../../../containers/logs/with_log_position'; +import { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview'; +import { ReduxSourceIdBridge, WithStreamItems } from '../../../containers/logs/with_stream_items'; +import { Source } from '../../../containers/source'; import { LogsToolbar } from './page_toolbar'; -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; -import { LogHighlightsBridge } from '../../containers/logs/log_highlights'; +import { LogHighlightsBridge } from '../../../containers/logs/log_highlights'; export const LogsPageLogsContent: React.FunctionComponent = () => { - const { derivedIndexPattern, source, sourceId, version } = useContext(Source.Context); + const { createDerivedIndexPattern, source, sourceId, version } = useContext(Source.Context); const { intervalSize, textScale, textWrap } = useContext(LogViewConfiguration.Context); const { setFlyoutVisibility, @@ -43,7 +42,8 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { flyoutItem, isLoading, } = useContext(LogFlyoutState.Context); - const { showLogsConfiguration } = useContext(SourceConfigurationFlyoutState.Context); + + const derivedIndexPattern = createDerivedIndexPattern('logs'); return ( <> @@ -103,7 +103,6 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { loadNewerItems={loadNewerEntries} reportVisibleInterval={reportVisiblePositions} scale={textScale} - showColumnConfiguration={showLogsConfiguration} target={targetPosition} wrap={textWrap} setFlyoutItem={setFlyoutId} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_no_indices_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx similarity index 82% rename from x-pack/legacy/plugins/infra/public/pages/logs/page_no_indices_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index a7392fafbeea..70de44c37ef5 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_no_indices_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -6,13 +6,16 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import React, { useContext } from 'react'; +import React from 'react'; import { UICapabilities } from 'ui/capabilities'; import { injectUICapabilities } from 'ui/capabilities/react'; -import { NoIndices } from '../../components/empty_states/no_indices'; -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; -import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; +import { NoIndices } from '../../../components/empty_states/no_indices'; +import { WithKibanaChrome } from '../../../containers/with_kibana_chrome'; +import { + ViewSourceConfigurationButton, + ViewSourceConfigurationButtonHrefBase, +} from '../../../components/source_configuration'; interface LogsPageNoIndicesContentProps { intl: InjectedIntl; @@ -22,7 +25,6 @@ interface LogsPageNoIndicesContentProps { export const LogsPageNoIndicesContent = injectUICapabilities( injectI18n((props: LogsPageNoIndicesContentProps) => { const { intl, uiCapabilities } = props; - const { showIndicesConfiguration } = useContext(SourceConfigurationFlyoutState.Context); return ( @@ -54,16 +56,15 @@ export const LogsPageNoIndicesContent = injectUICapabilities( {uiCapabilities.logs.configureSource ? ( - {intl.formatMessage({ id: 'xpack.infra.configureSourceActionLabel', defaultMessage: 'Change source configuration', })} - + ) : null} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx new file mode 100644 index 000000000000..b93cf48bde5e --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.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 { LogFlyout } from '../../../containers/logs/log_flyout'; +import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; +import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights'; +import { Source, useSource } from '../../../containers/source'; +import { useSourceId } from '../../../containers/source_id'; + +export const LogsPageProviders: React.FunctionComponent = ({ children }) => { + const [sourceId] = useSourceId(); + const source = useSource({ sourceId }); + + return ( + + + + + {children} + + + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.tsx new file mode 100644 index 000000000000..46cf3aab40e9 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { injectI18n } from '@kbn/i18n/react'; +import React, { useContext } from 'react'; + +import { AutocompleteField } from '../../../components/autocomplete_field'; +import { Toolbar } from '../../../components/eui'; +import { LogCustomizationMenu } from '../../../components/logging/log_customization_menu'; +import { LogHighlightsMenu } from '../../../components/logging/log_highlights_menu'; +import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights'; +import { LogMinimapScaleControls } from '../../../components/logging/log_minimap_scale_controls'; +import { LogTextScaleControls } from '../../../components/logging/log_text_scale_controls'; +import { LogTextWrapControls } from '../../../components/logging/log_text_wrap_controls'; +import { LogTimeControls } from '../../../components/logging/log_time_controls'; +import { LogFlyout } from '../../../containers/logs/log_flyout'; +import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; +import { WithLogFilter } from '../../../containers/logs/with_log_filter'; +import { WithLogPosition } from '../../../containers/logs/with_log_position'; +import { Source } from '../../../containers/source'; +import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion'; + +export const LogsToolbar = injectI18n(({ intl }) => { + const { createDerivedIndexPattern } = useContext(Source.Context); + const derivedIndexPattern = createDerivedIndexPattern('logs'); + const { + availableIntervalSizes, + availableTextScales, + intervalSize, + setIntervalSize, + setTextScale, + setTextWrap, + textScale, + textWrap, + } = useContext(LogViewConfiguration.Context); + + const { setSurroundingLogsId } = useContext(LogFlyout.Context); + + const { + setHighlightTerms, + loadLogEntryHighlightsRequest, + highlightTerms, + hasPreviousHighlight, + hasNextHighlight, + goToPreviousHighlight, + goToNextHighlight, + } = useContext(LogHighlightsState.Context); + return ( + + + + + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( + + {({ + applyFilterQueryFromKueryExpression, + filterQueryDraft, + isFilterQueryDraftValid, + setFilterQueryDraftFromKueryExpression, + }) => ( + { + setSurroundingLogsId(null); + setFilterQueryDraftFromKueryExpression(expression); + }} + onSubmit={(expression: string) => { + setSurroundingLogsId(null); + applyFilterQueryFromKueryExpression(expression); + }} + placeholder={intl.formatMessage({ + id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder', + defaultMessage: 'Search for log entries… (e.g. host.name:host-1)', + })} + suggestions={suggestions} + value={filterQueryDraft ? filterQueryDraft.expression : ''} + aria-label={intl.formatMessage({ + id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldAriaLabel', + defaultMessage: 'Search for log entries', + })} + /> + )} + + )} + + + + + + + + + + + highlightTerm.length > 0).length > 0 + } + goToPreviousHighlight={goToPreviousHighlight} + goToNextHighlight={goToNextHighlight} + hasPreviousHighlight={hasPreviousHighlight} + hasNextHighlight={hasNextHighlight} + /> + + + + {({ + visibleMidpointTime, + isAutoReloading, + jumpToTargetPositionTime, + startLiveStreaming, + stopLiveStreaming, + }) => ( + { + startLiveStreaming(interval); + setSurroundingLogsId(null); + }} + stopLiveStreaming={stopLiveStreaming} + /> + )} + + + + + ); +}); diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx index 1929b3351acb..dc2dcd0d4afe 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { GraphQLFormattedError } from 'graphql'; -import React from 'react'; +import React, { useCallback, useContext } from 'react'; import { UICapabilities } from 'ui/capabilities'; import { injectUICapabilities } from 'ui/capabilities/react'; import euiStyled, { EuiTheme, withTheme } from '../../../../../common/eui_styled_components'; @@ -28,19 +28,18 @@ import { InvalidNodeError } from '../../components/metrics/invalid_node'; import { MetricsSideNav } from '../../components/metrics/side_nav'; import { MetricsTimeControls } from '../../components/metrics/time_controls'; import { ColumnarPage, PageContent } from '../../components/page'; -import { SourceConfigurationFlyout } from '../../components/source_configuration'; -import { WithMetadata } from '../../containers/metadata/with_metadata'; import { WithMetrics } from '../../containers/metrics/with_metrics'; import { WithMetricsTime, WithMetricsTimeUrlState, } from '../../containers/metrics/with_metrics_time'; -import { WithSource } from '../../containers/with_source'; import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types'; import { Error, ErrorPageBody } from '../error'; import { layoutCreators } from './layouts'; import { InfraMetricLayoutSection } from './layouts/types'; -import { MetricDetailPageProviders } from './page_providers'; +import { withMetricPageProviders } from './page_providers'; +import { useMetadata } from '../../containers/metadata/use_metadata'; +import { Source } from '../../containers/source'; const DetailPageContent = euiStyled(PageContent)` overflow: auto; @@ -63,206 +62,186 @@ interface Props { uiCapabilities: UICapabilities; } -export const MetricDetail = injectUICapabilities( - withTheme( - injectI18n( - class extends React.PureComponent { - public static displayName = 'MetricDetailPage'; +export const MetricDetail = withMetricPageProviders( + injectUICapabilities( + withTheme( + injectI18n(({ intl, uiCapabilities, match, theme }: Props) => { + const nodeId = match.params.node; + const nodeType = match.params.type as InfraNodeType; + const layoutCreator = layoutCreators[nodeType]; + if (!layoutCreator) { + return ( + + ); + } + const { sourceId } = useContext(Source.Context); + const layouts = layoutCreator(theme); + const { name, filteredLayouts, loading: metadataLoading } = useMetadata( + nodeId, + nodeType, + layouts, + sourceId + ); + const breadcrumbs = [ + { + href: '#/', + text: intl.formatMessage({ + id: 'xpack.infra.header.infrastructureTitle', + defaultMessage: 'Infrastructure', + }), + }, + { text: name }, + ]; - public render() { - const { intl, uiCapabilities } = this.props; - const nodeId = this.props.match.params.node; - const nodeType = this.props.match.params.type as InfraNodeType; - const layoutCreator = layoutCreators[nodeType]; - if (!layoutCreator) { - return ( - - ); - } - const layouts = layoutCreator(this.props.theme); + const handleClick = useCallback( + (section: InfraMetricLayoutSection) => () => { + const id = section.linkToId || section.id; + const el = document.getElementById(id); + if (el) { + el.scrollIntoView(); + } + }, + [] + ); - return ( - - - {({ sourceId }) => ( - - {({ - timeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - }) => ( - - {({ name, filteredLayouts, loading: metadataLoading }) => { - const breadcrumbs = [ - { - href: '#/', - text: intl.formatMessage({ - id: 'xpack.infra.header.infrastructureTitle', - defaultMessage: 'Infrastructure', - }), - }, - { text: name }, - ]; - return ( - -
    - - - + {({ + timeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + }) => ( + +
    + + + + + {({ metrics, error, loading, refetch }) => { + if (error) { + const invalidNodeError = error.graphQLErrors.some( + (err: GraphQLFormattedError) => + err.code === InfraMetricsErrorCodes.invalid_node + ); + + return ( + <> + + intl.formatMessage( { - id: 'xpack.infra.metricDetailPage.documentTitle', - defaultMessage: 'Infrastructure | Metrics | {name}', + id: 'xpack.infra.metricDetailPage.documentTitleError', + defaultMessage: '{previousTitle} | Uh oh', }, { - name, + previousTitle, } - )} - /> - - - {({ metrics, error, loading, refetch }) => { - if (error) { - const invalidNodeError = error.graphQLErrors.some( - (err: GraphQLFormattedError) => - err.code === InfraMetricsErrorCodes.invalid_node - ); - - return ( - <> - - intl.formatMessage( - { - id: - 'xpack.infra.metricDetailPage.documentTitleError', - defaultMessage: '{previousTitle} | Uh oh', - }, - { - previousTitle, - } - ) - } + ) + } + /> + {invalidNodeError ? ( + + ) : ( + + )} + + ); + } + return ( + + + + {({ measureRef, bounds: { width = 0 } }) => { + return ( + + + + + + + +

    {name}

    +
    +
    + - {invalidNodeError ? ( - - ) : ( - - )} - - ); - } - return ( - - - - {({ measureRef, bounds: { width = 0 } }) => { - return ( - - - - - - - -

    {name}

    -
    -
    - -
    -
    -
    - - - 0 && isAutoReloading - ? false - : loading - } - refetch={refetch} - onChangeRangeTime={setTimeRange} - isLiveStreaming={isAutoReloading} - stopLiveStreaming={() => setAutoReload(false)} - /> - -
    -
    - ); - }} -
    -
    - ); - }} -
    -
    - - ); - }} - - )} - - )} - - - ); - } + + + - private handleClick = (section: InfraMetricLayoutSection) => () => { - const id = section.linkToId || section.id; - const el = document.getElementById(id); - if (el) { - el.scrollIntoView(); - } - }; - } + + 0 && isAutoReloading ? false : loading + } + refetch={refetch} + onChangeRangeTime={setTimeRange} + isLiveStreaming={isAutoReloading} + stopLiveStreaming={() => setAutoReload(false)} + /> + + + + ); + }} + + + ); + }} +
    +
    + + )} + + ); + }) ) ) ); diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/page_providers.tsx index 49f07837024f..5e43e79ab7c8 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/page_providers.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/page_providers.tsx @@ -6,14 +6,15 @@ import React from 'react'; -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; import { MetricsTimeContainer } from '../../containers/metrics/with_metrics_time'; import { Source } from '../../containers/source'; -export const MetricDetailPageProviders: React.FunctionComponent = ({ children }) => ( +export const withMetricPageProviders = (Component: React.ComponentType) => ( + props: T +) => ( - - {children} - + + + ); diff --git a/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx b/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx new file mode 100644 index 000000000000..daea6cfabdc2 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; +import { SourceConfigurationSettings } from '../../../components/source_configuration/source_configuration_settings'; + +interface SettingsPageProps { + uiCapabilities: UICapabilities; +} + +export const SettingsPage = injectUICapabilities(({ uiCapabilities }: SettingsPageProps) => ( + +)); diff --git a/x-pack/legacy/plugins/infra/public/routes.tsx b/x-pack/legacy/plugins/infra/public/routes.tsx index e9343c963549..dcdb40cb5ed7 100644 --- a/x-pack/legacy/plugins/infra/public/routes.tsx +++ b/x-pack/legacy/plugins/infra/public/routes.tsx @@ -37,6 +37,7 @@ const PageRouterComponent: React.SFC = ({ history, uiCapabilities } {uiCapabilities.infrastructure.show && ( )} + {uiCapabilities.logs.show && } {uiCapabilities.logs.show && } {uiCapabilities.infrastructure.show && ( diff --git a/x-pack/legacy/plugins/infra/server/graphql/index.ts b/x-pack/legacy/plugins/infra/server/graphql/index.ts index 552076c05e8e..81400b74f053 100644 --- a/x-pack/legacy/plugins/infra/server/graphql/index.ts +++ b/x-pack/legacy/plugins/infra/server/graphql/index.ts @@ -7,7 +7,6 @@ import { rootSchema } from '../../common/graphql/root/schema.gql'; import { sharedSchema } from '../../common/graphql/shared/schema.gql'; import { logEntriesSchema } from './log_entries/schema.gql'; -import { metadataSchema } from './metadata/schema.gql'; import { metricsSchema } from './metrics/schema.gql'; import { snapshotSchema } from './snapshot/schema.gql'; import { sourceStatusSchema } from './source_status/schema.gql'; @@ -16,7 +15,6 @@ import { sourcesSchema } from './sources/schema.gql'; export const schemas = [ rootSchema, sharedSchema, - metadataSchema, logEntriesSchema, snapshotSchema, sourcesSchema, diff --git a/x-pack/legacy/plugins/infra/server/graphql/metadata/index.ts b/x-pack/legacy/plugins/infra/server/graphql/metadata/index.ts deleted file mode 100644 index cda731bdaa9b..000000000000 --- a/x-pack/legacy/plugins/infra/server/graphql/metadata/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createMetadataResolvers } from './resolvers'; -export { metadataSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/infra/server/graphql/metadata/resolvers.ts b/x-pack/legacy/plugins/infra/server/graphql/metadata/resolvers.ts deleted file mode 100644 index 8d1b386af548..000000000000 --- a/x-pack/legacy/plugins/infra/server/graphql/metadata/resolvers.ts +++ /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 { InfraSourceResolvers } from '../../graphql/types'; -import { InfraMetadataDomain } from '../../lib/domains/metadata_domain'; -import { ChildResolverOf, InfraResolverOf } from '../../utils/typed_resolvers'; -import { QuerySourceResolver } from '../sources/resolvers'; - -type InfraSourceMetadataByNodeResolver = ChildResolverOf< - InfraResolverOf, - QuerySourceResolver ->; - -export const createMetadataResolvers = (libs: { - metadata: InfraMetadataDomain; -}): { - InfraSource: { - metadataByNode: InfraSourceMetadataByNodeResolver; - }; -} => ({ - InfraSource: { - async metadataByNode(source, args, { req }) { - const result = await libs.metadata.getMetadata(req, source.id, args.nodeId, args.nodeType); - return result; - }, - }, -}); diff --git a/x-pack/legacy/plugins/infra/server/graphql/metadata/schema.gql.ts b/x-pack/legacy/plugins/infra/server/graphql/metadata/schema.gql.ts deleted file mode 100644 index 8944e309d463..000000000000 --- a/x-pack/legacy/plugins/infra/server/graphql/metadata/schema.gql.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 gql from 'graphql-tag'; - -export const metadataSchema = gql` - "One metadata entry for a node." - type InfraNodeMetadata { - id: ID! - name: String! - features: [InfraNodeFeature!]! - } - - type InfraNodeFeature { - name: String! - source: String! - } - - extend type InfraSource { - "A hierarchy of metadata entries by node" - metadataByNode(nodeId: String!, nodeType: InfraNodeType!): InfraNodeMetadata! - } -`; diff --git a/x-pack/legacy/plugins/infra/server/graphql/types.ts b/x-pack/legacy/plugins/infra/server/graphql/types.ts index 619166b8b859..e223ac7b334a 100644 --- a/x-pack/legacy/plugins/infra/server/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/server/graphql/types.ts @@ -56,8 +56,6 @@ export interface InfraSource { configuration: InfraSourceConfiguration; /** The status of the source */ status: InfraSourceStatus; - /** A hierarchy of metadata entries by node */ - metadataByNode: InfraNodeMetadata; /** A consecutive span of log entries surrounding a point in time */ logEntriesAround: InfraLogEntryInterval; /** A consecutive span of log entries within an interval */ @@ -160,20 +158,6 @@ export interface InfraIndexField { /** Whether the field's values can be aggregated */ aggregatable: boolean; } -/** One metadata entry for a node. */ -export interface InfraNodeMetadata { - id: string; - - name: string; - - features: InfraNodeFeature[]; -} - -export interface InfraNodeFeature { - name: string; - - source: string; -} /** A consecutive sequence of log entries */ export interface InfraLogEntryInterval { /** The key corresponding to the start of the interval covered by the entries */ @@ -452,11 +436,6 @@ export interface SourceQueryArgs { /** The id of the source */ id: string; } -export interface MetadataByNodeInfraSourceArgs { - nodeId: string; - - nodeType: InfraNodeType; -} export interface LogEntriesAroundInfraSourceArgs { /** The sort key that corresponds to the point in time */ key: InfraTimeKeyInput; @@ -663,8 +642,6 @@ export namespace InfraSourceResolvers { configuration?: ConfigurationResolver; /** The status of the source */ status?: StatusResolver; - /** A hierarchy of metadata entries by node */ - metadataByNode?: MetadataByNodeResolver; /** A consecutive span of log entries surrounding a point in time */ logEntriesAround?: LogEntriesAroundResolver; /** A consecutive span of log entries within an interval */ @@ -711,17 +688,6 @@ export namespace InfraSourceResolvers { Parent = InfraSource, Context = InfraContext > = Resolver; - export type MetadataByNodeResolver< - R = InfraNodeMetadata, - Parent = InfraSource, - Context = InfraContext - > = Resolver; - export interface MetadataByNodeArgs { - nodeId: string; - - nodeType: InfraNodeType; - } - export type LogEntriesAroundResolver< R = InfraLogEntryInterval, Parent = InfraSource, @@ -1106,51 +1072,6 @@ export namespace InfraIndexFieldResolvers { Context = InfraContext > = Resolver; } -/** One metadata entry for a node. */ -export namespace InfraNodeMetadataResolvers { - export interface Resolvers { - id?: IdResolver; - - name?: NameResolver; - - features?: FeaturesResolver; - } - - export type IdResolver = Resolver< - R, - Parent, - Context - >; - export type NameResolver< - R = string, - Parent = InfraNodeMetadata, - Context = InfraContext - > = Resolver; - export type FeaturesResolver< - R = InfraNodeFeature[], - Parent = InfraNodeMetadata, - Context = InfraContext - > = Resolver; -} - -export namespace InfraNodeFeatureResolvers { - export interface Resolvers { - name?: NameResolver; - - source?: SourceResolver; - } - - export type NameResolver< - R = string, - Parent = InfraNodeFeature, - Context = InfraContext - > = Resolver; - export type SourceResolver< - R = string, - Parent = InfraNodeFeature, - Context = InfraContext - > = Resolver; -} /** A consecutive sequence of log entries */ export namespace InfraLogEntryIntervalResolvers { export interface Resolvers { diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index 25303a1a29de..98536f4c85d3 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -8,19 +8,18 @@ import { IResolvers, makeExecutableSchema } from 'graphql-tools'; import { initIpToHostName } from './routes/ip_to_hostname'; import { schemas } from './graphql'; import { createLogEntriesResolvers } from './graphql/log_entries'; -import { createMetadataResolvers } from './graphql/metadata'; import { createMetricResolvers } from './graphql/metrics/resolvers'; import { createSnapshotResolvers } from './graphql/snapshot'; import { createSourceStatusResolvers } from './graphql/source_status'; import { createSourcesResolvers } from './graphql/sources'; import { InfraBackendLibs } from './lib/infra_types'; -import { initLegacyLoggingRoutes } from './logging_legacy'; +import { initLogAnalysisGetLogEntryRateRoute } from './routes/log_analysis'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; +import { initMetadataRoute } from './routes/metadata'; export const initInfraServer = (libs: InfraBackendLibs) => { const schema = makeExecutableSchema({ resolvers: [ - createMetadataResolvers(libs) as IResolvers, createLogEntriesResolvers(libs) as IResolvers, createSnapshotResolvers(libs) as IResolvers, createSourcesResolvers(libs) as IResolvers, @@ -32,7 +31,8 @@ export const initInfraServer = (libs: InfraBackendLibs) => { libs.framework.registerGraphQLEndpoint('/api/infra/graphql', schema); - initLegacyLoggingRoutes(libs.framework); initIpToHostName(libs); + initLogAnalysisGetLogEntryRateRoute(libs); initMetricExplorerRoute(libs); + initMetadataRoute(libs); }; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 0fca1f35dc4d..2aa3bc3962eb 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -47,6 +47,11 @@ export interface InfraBackendFrameworkAdapter { method: 'indices.getAlias' | 'indices.get', options?: object ): Promise; + callWithRequest( + req: InfraFrameworkRequest, + method: 'ml.getBuckets', + options?: object + ): Promise; callWithRequest( req: InfraFrameworkRequest, method: string, @@ -54,6 +59,7 @@ export interface InfraBackendFrameworkAdapter { ): Promise; getIndexPatternsService(req: InfraFrameworkRequest): Legacy.IndexPatternsService; getSavedObjectsService(): Legacy.SavedObjectsService; + getSpaceId(request: InfraFrameworkRequest): string; makeTSVBRequest( req: InfraFrameworkRequest, model: InfraMetricModel, diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 7b322cbb27fc..400bc9c18ebf 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -112,10 +112,19 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework } } + const frozenIndicesParams = ['search', 'msearch'].includes(endpoint) + ? { + ignore_throttled: !includeFrozen, + } + : {}; + const fields = await callWithRequest( internalRequest, endpoint, - { ...params, ignore_throttled: !includeFrozen }, + { + ...params, + ...frozenIndicesParams, + }, ...rest ); return fields; @@ -137,6 +146,10 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework }); } + public getSpaceId(request: InfraFrameworkRequest): string { + return this.server.plugins.spaces.getSpaceId(request[internalInfraFrameworkRequest]); + } + public getSavedObjectsService() { return this.server.savedObjects; } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/adapter_types.ts deleted file mode 100644 index 3e44120a6c5c..000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/adapter_types.ts +++ /dev/null @@ -1,23 +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 { InfraSourceConfiguration } from '../../sources'; -import { InfraFrameworkRequest, InfraMetadataAggregationBucket } from '../framework'; - -export interface InfraMetricsAdapterResponse { - id: string; - name?: string; - buckets: InfraMetadataAggregationBucket[]; -} - -export interface InfraMetadataAdapter { - getMetricMetadata( - req: InfraFrameworkRequest, - sourceConfiguration: InfraSourceConfiguration, - nodeId: string, - nodeType: string - ): Promise; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.ts deleted file mode 100644 index d23b9b9bb142..000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/elasticsearch_metadata_adapter.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 { get } from 'lodash'; -import { InfraSourceConfiguration } from '../../sources'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraMetadataAggregationResponse, -} from '../framework'; -import { InfraMetadataAdapter, InfraMetricsAdapterResponse } from './adapter_types'; -import { NAME_FIELDS } from '../../constants'; - -export class ElasticsearchMetadataAdapter implements InfraMetadataAdapter { - private framework: InfraBackendFrameworkAdapter; - constructor(framework: InfraBackendFrameworkAdapter) { - this.framework = framework; - } - - public async getMetricMetadata( - req: InfraFrameworkRequest, - sourceConfiguration: InfraSourceConfiguration, - nodeId: string, - nodeType: 'host' | 'container' | 'pod' - ): Promise { - const idFieldName = getIdFieldName(sourceConfiguration, nodeType); - const metricQuery = { - allowNoIndices: true, - ignoreUnavailable: true, - index: sourceConfiguration.metricAlias, - body: { - query: { - bool: { - filter: { - term: { [idFieldName]: nodeId }, - }, - }, - }, - size: 0, - aggs: { - nodeName: { - terms: { - field: NAME_FIELDS[nodeType], - size: 1, - }, - }, - metrics: { - terms: { - field: 'event.dataset', - size: 1000, - }, - }, - }, - }, - }; - - const response = await this.framework.callWithRequest< - any, - { metrics?: InfraMetadataAggregationResponse; nodeName?: InfraMetadataAggregationResponse } - >(req, 'search', metricQuery); - - const buckets = - response.aggregations && response.aggregations.metrics - ? response.aggregations.metrics.buckets - : []; - - return { - id: nodeId, - name: get(response, ['aggregations', 'nodeName', 'buckets', 0, 'key'], nodeId), - buckets, - }; - } -} - -const getIdFieldName = (sourceConfiguration: InfraSourceConfiguration, nodeType: string) => { - switch (nodeType) { - case 'host': - return sourceConfiguration.fields.host; - case 'container': - return sourceConfiguration.fields.container; - default: - return sourceConfiguration.fields.pod; - } -}; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/index.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/index.ts deleted file mode 100644 index 4e09b5d0e9e2..000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metadata/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './adapter_types'; diff --git a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts index f1691c9f3c7d..215c41bcf6b7 100644 --- a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts @@ -10,14 +10,13 @@ import { InfraKibanaConfigurationAdapter } from '../adapters/configuration/kiban import { FrameworkFieldsAdapter } from '../adapters/fields/framework_fields_adapter'; import { InfraKibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaLogEntriesAdapter } from '../adapters/log_entries/kibana_log_entries_adapter'; -import { ElasticsearchMetadataAdapter } from '../adapters/metadata/elasticsearch_metadata_adapter'; import { KibanaMetricsAdapter } from '../adapters/metrics/kibana_metrics_adapter'; import { InfraElasticsearchSourceStatusAdapter } from '../adapters/source_status'; import { InfraFieldsDomain } from '../domains/fields_domain'; import { InfraLogEntriesDomain } from '../domains/log_entries_domain'; -import { InfraMetadataDomain } from '../domains/metadata_domain'; import { InfraMetricsDomain } from '../domains/metrics_domain'; import { InfraBackendLibs, InfraDomainLibs } from '../infra_types'; +import { InfraLogAnalysis } from '../log_analysis'; import { InfraSnapshot } from '../snapshot'; import { InfraSourceStatus } from '../source_status'; import { InfraSources } from '../sources'; @@ -33,11 +32,9 @@ export function compose(server: Server): InfraBackendLibs { sources, }); const snapshot = new InfraSnapshot({ sources, framework }); + const logAnalysis = new InfraLogAnalysis({ framework }); const domainLibs: InfraDomainLibs = { - metadata: new InfraMetadataDomain(new ElasticsearchMetadataAdapter(framework), { - sources, - }), fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { sources, }), @@ -50,6 +47,7 @@ export function compose(server: Server): InfraBackendLibs { const libs: InfraBackendLibs = { configuration, framework, + logAnalysis, snapshot, sources, sourceStatus, diff --git a/x-pack/legacy/plugins/infra/server/lib/constants.ts b/x-pack/legacy/plugins/infra/server/lib/constants.ts index c964e691058c..4f2fa561da0c 100644 --- a/x-pack/legacy/plugins/infra/server/lib/constants.ts +++ b/x-pack/legacy/plugins/infra/server/lib/constants.ts @@ -20,3 +20,5 @@ export const IP_FIELDS = { [InfraNodeType.pod]: 'kubernetes.pod.ip', [InfraNodeType.container]: 'container.ip_address', }; + +export const CLOUD_METRICS_MODULES = ['aws']; diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/index.ts b/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/index.ts deleted file mode 100644 index 8095e8424873..000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './metadata_domain'; diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts deleted file mode 100644 index 5fb86df5a633..000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/domains/metadata_domain/metadata_domain.ts +++ /dev/null @@ -1,45 +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 { InfraFrameworkRequest, InfraMetadataAggregationBucket } from '../../adapters/framework'; -import { InfraMetadataAdapter } from '../../adapters/metadata'; -import { InfraSources } from '../../sources'; - -export class InfraMetadataDomain { - constructor( - private readonly adapter: InfraMetadataAdapter, - private readonly libs: { sources: InfraSources } - ) {} - - public async getMetadata( - req: InfraFrameworkRequest, - sourceId: string, - nodeId: string, - nodeType: string - ) { - const { configuration } = await this.libs.sources.getSourceConfiguration(req, sourceId); - const metricsPromise = this.adapter.getMetricMetadata(req, configuration, nodeId, nodeType); - - const metrics = await metricsPromise; - - const metricMetadata = pickMetadata(metrics.buckets).map(entry => { - return { name: entry, source: 'metrics' }; - }); - - const id = metrics.id; - const name = metrics.name || id; - return { id, name, features: metricMetadata }; - } -} - -const pickMetadata = (buckets: InfraMetadataAggregationBucket[]): string[] => { - if (buckets) { - const metadata = buckets.map(bucket => bucket.key); - return metadata; - } else { - return []; - } -}; diff --git a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts index c63a82c137e1..b436bb7e4fe5 100644 --- a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts @@ -9,14 +9,13 @@ import { InfraConfigurationAdapter } from './adapters/configuration'; import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from './adapters/framework'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; -import { InfraMetadataDomain } from './domains/metadata_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; +import { InfraLogAnalysis } from './log_analysis/log_analysis'; import { InfraSnapshot } from './snapshot'; -import { InfraSourceStatus } from './source_status'; import { InfraSources } from './sources'; +import { InfraSourceStatus } from './source_status'; export interface InfraDomainLibs { - metadata: InfraMetadataDomain; fields: InfraFieldsDomain; logEntries: InfraLogEntriesDomain; metrics: InfraMetricsDomain; @@ -25,6 +24,7 @@ export interface InfraDomainLibs { export interface InfraBackendLibs extends InfraDomainLibs { configuration: InfraConfigurationAdapter; framework: InfraBackendFrameworkAdapter; + logAnalysis: InfraLogAnalysis; snapshot: InfraSnapshot; sources: InfraSources; sourceStatus: InfraSourceStatus; diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/errors.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/errors.ts new file mode 100644 index 000000000000..dc5c87c61fdc --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/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 NoLogRateResultsIndexError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/index.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/index.ts new file mode 100644 index 000000000000..0b58c71c1db7 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/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 * from './errors'; +export * from './log_analysis'; diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts new file mode 100644 index 000000000000..e0f98093b87b --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 rt from 'io-ts'; + +import { getJobId } from '../../../common/log_analysis'; +import { throwErrors, createPlainError } from '../../../common/runtime_types'; +import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../adapters/framework'; +import { NoLogRateResultsIndexError } from './errors'; + +const ML_ANOMALY_INDEX_PREFIX = '.ml-anomalies-'; + +export class InfraLogAnalysis { + constructor( + private readonly libs: { + framework: InfraBackendFrameworkAdapter; + } + ) {} + + public getJobIds(request: InfraFrameworkRequest, sourceId: string) { + return { + logEntryRate: getJobId(this.libs.framework.getSpaceId(request), sourceId, 'log-entry-rate'), + }; + } + + public async getLogEntryRateBuckets( + request: InfraFrameworkRequest, + sourceId: string, + startTime: number, + endTime: number, + bucketDuration: number + ) { + const logRateJobId = this.getJobIds(request, sourceId).logEntryRate; + + const mlModelPlotResponse = await this.libs.framework.callWithRequest(request, 'search', { + allowNoIndices: true, + body: { + query: { + bool: { + filter: [ + { + range: { + timestamp: { + gte: startTime, + lt: endTime, + }, + }, + }, + { + terms: { + result_type: ['model_plot', 'record'], + }, + }, + { + term: { + detector_index: { + value: 0, + }, + }, + }, + ], + }, + }, + aggs: { + timestamp_buckets: { + date_histogram: { + field: 'timestamp', + fixed_interval: `${bucketDuration}ms`, + }, + aggs: { + filter_model_plot: { + filter: { + term: { + result_type: 'model_plot', + }, + }, + aggs: { + stats_model_lower: { + stats: { + field: 'model_lower', + }, + }, + stats_model_upper: { + stats: { + field: 'model_upper', + }, + }, + stats_actual: { + stats: { + field: 'actual', + }, + }, + }, + }, + filter_records: { + filter: { + term: { + result_type: 'record', + }, + }, + aggs: { + top_hits_record: { + top_hits: { + _source: Object.keys(logRateMlRecordRT.props), + size: 100, + sort: [ + { + timestamp: 'asc', + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + ignoreUnavailable: true, + index: `${ML_ANOMALY_INDEX_PREFIX}${logRateJobId}`, + size: 0, + trackScores: false, + trackTotalHits: false, + }); + + if (mlModelPlotResponse._shards.total === 0) { + throw new NoLogRateResultsIndexError( + `Failed to find ml result index for job ${logRateJobId}.` + ); + } + + const mlModelPlotBuckets = logRateModelPlotResponseRT + .decode(mlModelPlotResponse) + .map(response => response.aggregations.timestamp_buckets.buckets) + .getOrElseL(throwErrors(createPlainError)); + + return mlModelPlotBuckets.map(bucket => ({ + anomalies: bucket.filter_records.top_hits_record.hits.hits.map(({ _source: record }) => ({ + actualLogEntryRate: record.actual[0], + anomalyScore: record.record_score, + duration: record.bucket_span * 1000, + startTime: record.timestamp, + typicalLogEntryRate: record.typical[0], + })), + duration: bucketDuration, + logEntryRateStats: bucket.filter_model_plot.stats_actual, + modelLowerBoundStats: bucket.filter_model_plot.stats_model_lower, + modelUpperBoundStats: bucket.filter_model_plot.stats_model_upper, + startTime: bucket.key, + })); + } +} + +const logRateMlRecordRT = rt.type({ + actual: rt.array(rt.number), + bucket_span: rt.number, + record_score: rt.number, + timestamp: rt.number, + typical: rt.array(rt.number), +}); + +const logRateStatsAggregationRT = rt.type({ + avg: rt.union([rt.number, rt.null]), + count: rt.number, + max: rt.union([rt.number, rt.null]), + min: rt.union([rt.number, rt.null]), + sum: rt.number, +}); + +const logRateModelPlotResponseRT = rt.type({ + aggregations: rt.type({ + timestamp_buckets: rt.type({ + buckets: rt.array( + rt.type({ + key: rt.number, + filter_records: rt.type({ + doc_count: rt.number, + top_hits_record: rt.type({ + hits: rt.type({ + hits: rt.array( + rt.type({ + _source: logRateMlRecordRT, + }) + ), + }), + }), + }), + filter_model_plot: rt.type({ + doc_count: rt.number, + stats_actual: logRateStatsAggregationRT, + stats_model_lower: logRateStatsAggregationRT, + stats_model_upper: logRateStatsAggregationRT, + }), + }) + ), + }), + }), +}); diff --git a/x-pack/legacy/plugins/infra/server/logging_legacy/adjacent_search_results.ts b/x-pack/legacy/plugins/infra/server/logging_legacy/adjacent_search_results.ts deleted file mode 100644 index 55113c3056e4..000000000000 --- a/x-pack/legacy/plugins/infra/server/logging_legacy/adjacent_search_results.ts +++ /dev/null @@ -1,189 +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 { boomify } from 'boom'; -import { SearchParams } from 'elasticsearch'; -import * as Joi from 'joi'; - -import { - AdjacentSearchResultsApiPostPayload, - AdjacentSearchResultsApiPostResponse, -} from '../../common/http_api'; -import { LogEntryFieldsMapping, LogEntryTime } from '../../common/log_entry'; -import { SearchResult } from '../../common/log_search_result'; -import { - InfraBackendFrameworkAdapter, - InfraDatabaseSearchResponse, - InfraWrappableRequest, -} from '../lib/adapters/framework'; -import { convertHitToSearchResult } from './converters'; -import { isHighlightedHit, SortedHit } from './elasticsearch'; -import { fetchLatestTime } from './latest_log_entries'; -import { indicesSchema, logEntryFieldsMappingSchema, logEntryTimeSchema } from './schemas'; - -const INITIAL_HORIZON_OFFSET = 1000 * 60 * 60 * 24; -const MAX_HORIZON = 9999999999999; - -export const initAdjacentSearchResultsRoutes = (framework: InfraBackendFrameworkAdapter) => { - const callWithRequest = framework.callWithRequest; - - framework.registerRoute< - InfraWrappableRequest, - Promise - >({ - options: { - validate: { - payload: Joi.object().keys({ - after: Joi.number() - .min(0) - .default(0), - before: Joi.number() - .min(0) - .default(0), - fields: logEntryFieldsMappingSchema.required(), - indices: indicesSchema.required(), - query: Joi.string().required(), - target: logEntryTimeSchema.required(), - }), - }, - }, - handler: async (request, h) => { - const timings = { - esRequestSent: Date.now(), - esResponseProcessed: 0, - }; - - try { - const search = (params: SearchParams) => - callWithRequest(request, 'search', params); - - const latestTime = await fetchLatestTime( - search, - request.payload.indices, - request.payload.fields.time - ); - const searchResultsAfterTarget = await fetchSearchResults( - search, - request.payload.indices, - request.payload.fields, - { - tiebreaker: request.payload.target.tiebreaker - 1, - time: request.payload.target.time, - }, - request.payload.after, - 'asc', - request.payload.query, - request.payload.target.time + INITIAL_HORIZON_OFFSET, - latestTime - ); - const searchResultsBeforeTarget = (await fetchSearchResults( - search, - request.payload.indices, - request.payload.fields, - request.payload.target, - request.payload.before, - 'desc', - request.payload.query, - request.payload.target.time - INITIAL_HORIZON_OFFSET - )).reverse(); - - timings.esResponseProcessed = Date.now(); - - return { - results: { - after: searchResultsAfterTarget, - before: searchResultsBeforeTarget, - }, - timings, - }; - } catch (requestError) { - throw boomify(requestError); - } - }, - method: 'POST', - path: '/api/logging/adjacent-search-results', - }); -}; - -export async function fetchSearchResults( - search: (params: SearchParams) => Promise>, - indices: string[], - fields: LogEntryFieldsMapping, - target: LogEntryTime, - size: number, - direction: 'asc' | 'desc', - query: string, - horizon: number, - maxHorizon: number = MAX_HORIZON -): Promise { - if (size <= 0) { - return []; - } - - const request = { - allowNoIndices: true, - body: { - _source: false, - highlight: { - boundary_scanner: 'word', - fields: { - [fields.message]: {}, - }, - fragment_size: 1, - number_of_fragments: 100, - post_tags: [''], - pre_tags: [''], - }, - query: { - bool: { - filter: [ - { - query_string: { - default_field: fields.message, - default_operator: 'AND', - query, - }, - }, - { - range: { - [fields.time]: { - [direction === 'asc' ? 'gte' : 'lte']: target.time, - [direction === 'asc' ? 'lte' : 'gte']: horizon, - }, - }, - }, - ], - }, - }, - search_after: [target.time, target.tiebreaker], - size, - sort: [{ [fields.time]: direction }, { [fields.tiebreaker]: direction }], - }, - ignoreUnavailable: true, - index: indices, - }; - const response = await search(request); - - const hits = response.hits.hits as SortedHit[]; - const nextHorizon = horizon + (horizon - target.time); - - if (hits.length >= size || nextHorizon < 0 || nextHorizon > maxHorizon) { - const filteredHits = hits.filter(isHighlightedHit); - return filteredHits.map(convertHitToSearchResult(fields)); - } else { - return fetchSearchResults( - search, - indices, - fields, - target, - size, - direction, - query, - nextHorizon, - maxHorizon - ); - } -} diff --git a/x-pack/legacy/plugins/infra/server/logging_legacy/contained_search_results.ts b/x-pack/legacy/plugins/infra/server/logging_legacy/contained_search_results.ts deleted file mode 100644 index dbd8b1f4202d..000000000000 --- a/x-pack/legacy/plugins/infra/server/logging_legacy/contained_search_results.ts +++ /dev/null @@ -1,135 +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 * as Boom from 'boom'; -import { SearchParams } from 'elasticsearch'; -import * as Joi from 'joi'; - -import { - ContainedSearchResultsApiPostPayload, - ContainedSearchResultsApiPostResponse, -} from '../../common/http_api'; -import { isLessOrEqual, LogEntryFieldsMapping, LogEntryTime } from '../../common/log_entry'; -import { SearchResult } from '../../common/log_search_result'; -import { - InfraBackendFrameworkAdapter, - InfraDatabaseSearchResponse, - InfraWrappableRequest, -} from '../lib/adapters/framework'; -import { convertHitToSearchResult } from './converters'; -import { isHighlightedHit, SortedHit } from './elasticsearch'; -import { indicesSchema, logEntryFieldsMappingSchema, logEntryTimeSchema } from './schemas'; - -export const initContainedSearchResultsRoutes = (framework: InfraBackendFrameworkAdapter) => { - const callWithRequest = framework.callWithRequest; - - framework.registerRoute< - InfraWrappableRequest, - Promise - >({ - options: { - validate: { - payload: Joi.object().keys({ - end: logEntryTimeSchema.required(), - fields: logEntryFieldsMappingSchema.required(), - indices: indicesSchema.required(), - query: Joi.string().required(), - start: logEntryTimeSchema.required(), - }), - }, - }, - handler: async request => { - const timings = { - esRequestSent: Date.now(), - esResponseProcessed: 0, - }; - - try { - const search = (params: SearchParams) => - callWithRequest(request, 'search', params); - - const searchResults = await fetchSearchResultsBetween( - search, - request.payload.indices, - request.payload.fields, - request.payload.start, - request.payload.end, - request.payload.query - ); - - timings.esResponseProcessed = Date.now(); - - return { - results: searchResults, - timings, - }; - } catch (requestError) { - throw Boom.boomify(requestError); - } - }, - method: 'POST', - path: '/api/logging/contained-search-results', - }); -}; - -export async function fetchSearchResultsBetween( - search: (params: SearchParams) => Promise>, - indices: string[], - fields: LogEntryFieldsMapping, - start: LogEntryTime, - end: LogEntryTime, - query: string -): Promise { - const request = { - allowNoIndices: true, - body: { - _source: false, - highlight: { - boundary_scanner: 'word', - fields: { - [fields.message]: {}, - }, - fragment_size: 1, - number_of_fragments: 100, - post_tags: [''], - pre_tags: [''], - }, - query: { - bool: { - filter: [ - { - query_string: { - default_field: fields.message, - default_operator: 'AND', - query, - }, - }, - { - range: { - [fields.time]: { - gte: start.time, - lte: end.time, - }, - }, - }, - ], - }, - }, - search_after: [start.time, start.tiebreaker - 1], - size: 10000, - sort: [{ [fields.time]: 'asc' }, { [fields.tiebreaker]: 'asc' }], - }, - ignoreUnavailable: true, - index: indices, - }; - const response = await search(request); - - const hits = response.hits.hits as SortedHit[]; - const filteredHits = hits - .filter(hit => isLessOrEqual({ time: hit.sort[0], tiebreaker: hit.sort[1] }, end)) - .filter(isHighlightedHit); - return filteredHits.map(convertHitToSearchResult(fields)); -} diff --git a/x-pack/legacy/plugins/infra/server/logging_legacy/converters.ts b/x-pack/legacy/plugins/infra/server/logging_legacy/converters.ts deleted file mode 100644 index 164981c2dbe1..000000000000 --- a/x-pack/legacy/plugins/infra/server/logging_legacy/converters.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import invert from 'lodash/fp/invert'; -import mapKeys from 'lodash/fp/mapKeys'; - -import { LogEntryFieldsMapping } from '../../common/log_entry'; -import { SearchResult } from '../../common/log_search_result'; -import { SearchSummaryBucket } from '../../common/log_search_summary'; -import { - DateHistogramResponse, - HighlightedHit, - Hit, - HitsBucket, - isBucketWithAggregation, -} from './elasticsearch'; - -export const convertHitToSearchResult = (fields: LogEntryFieldsMapping) => { - const invertedFields = invert(fields); - return (hit: HighlightedHit): SearchResult => { - const matches = mapKeys(key => invertedFields[key], hit.highlight || {}); - return { - fields: { - tiebreaker: hit.sort[1], // use the sort property to get the normalized values - time: hit.sort[0], - }, - gid: getHitGid(hit), - matches, - }; - }; -}; - -export const convertDateHistogramToSearchSummaryBuckets = ( - fields: LogEntryFieldsMapping, - end: number -) => (buckets: DateHistogramResponse['buckets']): SearchSummaryBucket[] => - buckets.reduceRight( - ({ previousStart, aggregatedBuckets }, bucket) => { - const representative = - isBucketWithAggregation(bucket, 'top_entries') && - bucket.top_entries.hits.hits.length > 0 - ? convertHitToSearchResult(fields)(bucket.top_entries.hits.hits[0]) - : null; - return { - aggregatedBuckets: [ - ...(representative - ? [ - { - count: bucket.doc_count, - end: previousStart, - representative, - start: bucket.key, - }, - ] - : []), - ...aggregatedBuckets, - ], - previousStart: bucket.key, - }; - }, - { previousStart: end, aggregatedBuckets: [] } as { - previousStart: number; - aggregatedBuckets: SearchSummaryBucket[]; - } - ).aggregatedBuckets; - -const getHitGid = (hit: Hit): string => `${hit._index}:${hit._type}:${hit._id}`; diff --git a/x-pack/legacy/plugins/infra/server/logging_legacy/elasticsearch.ts b/x-pack/legacy/plugins/infra/server/logging_legacy/elasticsearch.ts deleted file mode 100644 index 020b9ae7ba2c..000000000000 --- a/x-pack/legacy/plugins/infra/server/logging_legacy/elasticsearch.ts +++ /dev/null @@ -1,79 +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 { MSearchParams, MSearchResponse, SearchParams, SearchResponse } from 'elasticsearch'; - -export interface ESCluster { - callWithRequest( - request: any, - endpoint: 'msearch', - clientOptions: MSearchParams, - options?: object - ): Promise>; - callWithRequest( - request: any, - endpoint: 'search', - clientOptions: SearchParams, - options?: object - ): Promise>; - callWithRequest( - request: any, - endpoint: string, - clientOptions?: object, - options?: object - ): Promise; -} - -export type Hit = SearchResponse['hits']['hits'][0]; - -export interface SortedHit extends Hit { - sort: any[]; - _source: { - [field: string]: any; - }; -} - -export interface HighlightedHit extends SortedHit { - highlight?: { - [field: string]: string[]; - }; -} - -export const isHighlightedHit = (hit: Hit): hit is HighlightedHit => !!hit.highlight; - -export interface DateHistogramBucket { - key: number; - key_as_string: string; - doc_count: number; -} - -export interface HitsBucket { - hits: { - total: number; - max_score: number | null; - hits: SortedHit[]; - }; -} - -export interface DateHistogramResponse { - buckets: DateHistogramBucket[]; -} - -export type WithSubAggregation< - SubAggregationType, - SubAggregationName extends string, - BucketType -> = BucketType & { [subAggregationName in SubAggregationName]: SubAggregationType }; - -export const isBucketWithAggregation = < - SubAggregationType extends object, - SubAggregationName extends string = any, - BucketType extends object = {} ->( - bucket: BucketType, - aggregationName: SubAggregationName -): bucket is WithSubAggregation => - aggregationName in bucket; diff --git a/x-pack/legacy/plugins/infra/server/logging_legacy/index.ts b/x-pack/legacy/plugins/infra/server/logging_legacy/index.ts deleted file mode 100644 index e19365524601..000000000000 --- a/x-pack/legacy/plugins/infra/server/logging_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 { InfraBackendFrameworkAdapter } from '../lib/adapters/framework'; -import { initAdjacentSearchResultsRoutes } from './adjacent_search_results'; -import { initContainedSearchResultsRoutes } from './contained_search_results'; -import { initSearchSummaryRoutes } from './search_summary'; - -export const initLegacyLoggingRoutes = (framework: InfraBackendFrameworkAdapter) => { - initAdjacentSearchResultsRoutes(framework); - initContainedSearchResultsRoutes(framework); - initSearchSummaryRoutes(framework); -}; diff --git a/x-pack/legacy/plugins/infra/server/logging_legacy/latest_log_entries.ts b/x-pack/legacy/plugins/infra/server/logging_legacy/latest_log_entries.ts deleted file mode 100644 index 2b40309b510d..000000000000 --- a/x-pack/legacy/plugins/infra/server/logging_legacy/latest_log_entries.ts +++ /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 { SearchParams } from 'elasticsearch'; - -import { InfraDatabaseSearchResponse } from '../lib/adapters/framework'; - -export async function fetchLatestTime( - search: ( - params: SearchParams - ) => Promise>, - indices: string[], - timeField: string -): Promise { - const response = await search({ - allowNoIndices: true, - body: { - aggregations: { - max_time: { - max: { - field: timeField, - }, - }, - }, - query: { - match_all: {}, - }, - size: 0, - }, - ignoreUnavailable: true, - index: indices, - }); - - if (response.aggregations && response.aggregations.max_time) { - return response.aggregations.max_time.value; - } else { - return 0; - } -} diff --git a/x-pack/legacy/plugins/infra/server/logging_legacy/schemas.ts b/x-pack/legacy/plugins/infra/server/logging_legacy/schemas.ts deleted file mode 100644 index 12dc60d369bd..000000000000 --- a/x-pack/legacy/plugins/infra/server/logging_legacy/schemas.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 * as Joi from 'joi'; - -export const timestampSchema = Joi.number() - .integer() - .min(0); - -export const logEntryFieldsMappingSchema = Joi.object().keys({ - message: Joi.string().required(), - tiebreaker: Joi.string().required(), - time: Joi.string().required(), -}); - -export const logEntryTimeSchema = Joi.object().keys({ - tiebreaker: Joi.number().integer(), - time: timestampSchema, -}); - -export const indicesSchema = Joi.array().items(Joi.string()); - -export const summaryBucketSizeSchema = Joi.object().keys({ - unit: Joi.string() - .valid(['y', 'M', 'w', 'd', 'h', 'm', 's']) - .required(), - value: Joi.number() - .integer() - .min(0) - .required(), -}); diff --git a/x-pack/legacy/plugins/infra/server/logging_legacy/search_summary.ts b/x-pack/legacy/plugins/infra/server/logging_legacy/search_summary.ts deleted file mode 100644 index b51d4d4cabbb..000000000000 --- a/x-pack/legacy/plugins/infra/server/logging_legacy/search_summary.ts +++ /dev/null @@ -1,156 +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 * as Boom from 'boom'; -import { SearchParams } from 'elasticsearch'; -import * as Joi from 'joi'; - -import { SearchSummaryApiPostPayload, SearchSummaryApiPostResponse } from '../../common/http_api'; -import { LogEntryFieldsMapping } from '../../common/log_entry'; -import { SearchSummaryBucket } from '../../common/log_search_summary'; -import { SummaryBucketSize } from '../../common/log_summary'; -import { - InfraBackendFrameworkAdapter, - InfraDatabaseSearchResponse, - InfraWrappableRequest, -} from '../lib/adapters/framework'; -import { convertDateHistogramToSearchSummaryBuckets } from './converters'; -import { DateHistogramResponse } from './elasticsearch'; -import { - indicesSchema, - logEntryFieldsMappingSchema, - summaryBucketSizeSchema, - timestampSchema, -} from './schemas'; - -export const initSearchSummaryRoutes = (framework: InfraBackendFrameworkAdapter) => { - const callWithRequest = framework.callWithRequest; - - framework.registerRoute< - InfraWrappableRequest, - Promise - >({ - options: { - validate: { - payload: Joi.object().keys({ - bucketSize: summaryBucketSizeSchema.required(), - end: timestampSchema.required(), - fields: logEntryFieldsMappingSchema.required(), - indices: indicesSchema.required(), - query: Joi.string().required(), - start: timestampSchema.required(), - }), - }, - }, - handler: async request => { - const timings = { - esRequestSent: Date.now(), - esResponseProcessed: 0, - }; - - try { - const search = (params: SearchParams) => - callWithRequest(request, 'search', params); - const summaryBuckets = await fetchSummaryBuckets( - search, - request.payload.indices, - request.payload.fields, - request.payload.start, - request.payload.end, - request.payload.bucketSize, - request.payload.query - ); - - timings.esResponseProcessed = Date.now(); - - return { - buckets: summaryBuckets, - timings, - }; - } catch (requestError) { - throw Boom.boomify(requestError); - } - }, - method: 'POST', - path: '/api/logging/search-summary', - }); -}; - -async function fetchSummaryBuckets( - search: ( - params: SearchParams - ) => Promise>, - indices: string[], - fields: LogEntryFieldsMapping, - start: number, - end: number, - bucketSize: { - unit: SummaryBucketSize; - value: number; - }, - query: string -): Promise { - const response = await search({ - allowNoIndices: true, - body: { - aggregations: { - count_by_date: { - aggregations: { - top_entries: { - top_hits: { - _source: [fields.message], - size: 1, - sort: [{ [fields.time]: 'desc' }, { [fields.tiebreaker]: 'desc' }], - }, - }, - }, - date_histogram: { - extended_bounds: { - max: end, - min: start, - }, - field: fields.time, - interval: `${bucketSize.value}${bucketSize.unit}`, - min_doc_count: 0, - }, - }, - }, - query: { - bool: { - filter: [ - { - query_string: { - default_field: fields.message, - default_operator: 'AND', - query, - }, - }, - { - range: { - [fields.time]: { - format: 'epoch_millis', - gte: start, - lt: end, - }, - }, - }, - ], - }, - }, - size: 0, - }, - ignoreUnavailable: true, - index: indices, - }); - - if (response.aggregations && response.aggregations.count_by_date) { - return convertDateHistogramToSearchSummaryBuckets(fields, end)( - response.aggregations.count_by_date.buckets - ); - } else { - return []; - } -} diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts new file mode 100644 index 000000000000..38684cb22e23 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/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 './results'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/index.ts new file mode 100644 index 000000000000..174942127771 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/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 './log_entry_rate'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts new file mode 100644 index 000000000000..58517093d531 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.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 Boom from 'boom'; + +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { + LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, + getLogEntryRateRequestPayloadRT, + getLogEntryRateSuccessReponsePayloadRT, +} from '../../../../common/http_api/log_analysis'; +import { throwErrors } from '../../../../common/runtime_types'; +import { NoLogRateResultsIndexError } from '../../../lib/log_analysis'; + +export const initLogAnalysisGetLogEntryRateRoute = ({ + framework, + logAnalysis, +}: InfraBackendLibs) => { + framework.registerRoute({ + method: 'POST', + path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, + handler: async (req, res) => { + const payload = getLogEntryRateRequestPayloadRT + .decode(req.payload) + .getOrElseL(throwErrors(Boom.badRequest)); + + const logEntryRateBuckets = await logAnalysis + .getLogEntryRateBuckets( + req, + payload.data.sourceId, + payload.data.timeRange.startTime, + payload.data.timeRange.endTime, + payload.data.bucketDuration + ) + .catch(err => { + if (err instanceof NoLogRateResultsIndexError) { + throw Boom.boomify(err, { statusCode: 404 }); + } + + throw Boom.boomify(err, { statusCode: ('statusCode' in err && err.statusCode) || 500 }); + }); + + return res.response( + getLogEntryRateSuccessReponsePayloadRT.encode({ + data: { + bucketDuration: payload.data.bucketDuration, + histogramBuckets: logEntryRateBuckets, + }, + }) + ); + }, + }); +}; diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts new file mode 100644 index 000000000000..34b3448b8607 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/index.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 Boom, { boomify } from 'boom'; +import { get } from 'lodash'; +import { + InfraMetadata, + InfraMetadataWrappedRequest, + InfraMetadataFeature, + InfraMetadataRequestRT, + InfraMetadataRT, +} from '../../../common/http_api/metadata_api'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { getMetricMetadata } from './lib/get_metric_metadata'; +import { pickFeatureName } from './lib/pick_feature_name'; +import { getCloudMetricsMetadata } from './lib/get_cloud_metric_metadata'; +import { getNodeInfo } from './lib/get_node_info'; +import { throwErrors } from '../../../common/runtime_types'; + +export const initMetadataRoute = (libs: InfraBackendLibs) => { + const { framework } = libs; + + framework.registerRoute>({ + method: 'POST', + path: '/api/infra/metadata', + handler: async req => { + try { + const { nodeId, nodeType, sourceId } = InfraMetadataRequestRT.decode( + req.payload + ).getOrElseL(throwErrors(Boom.badRequest)); + + const { configuration } = await libs.sources.getSourceConfiguration(req, sourceId); + const metricsMetadata = await getMetricMetadata( + framework, + req, + configuration, + nodeId, + nodeType + ); + const metricFeatures = pickFeatureName(metricsMetadata.buckets).map( + nameToFeature('metrics') + ); + + const info = await getNodeInfo(framework, req, configuration, nodeId, nodeType); + const cloudInstanceId = get(info, 'cloud.instance.id'); + + const cloudMetricsMetadata = cloudInstanceId + ? await getCloudMetricsMetadata(framework, req, configuration, cloudInstanceId) + : { buckets: [] }; + const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( + nameToFeature('metrics') + ); + + const id = metricsMetadata.id; + const name = metricsMetadata.name || id; + return InfraMetadataRT.decode({ + id, + name, + features: [...metricFeatures, ...cloudMetricsFeatures], + info, + }).getOrElseL(throwErrors(Boom.badImplementation)); + } catch (error) { + throw boomify(error); + } + }, + }); +}; + +const nameToFeature = (source: string) => (name: string): InfraMetadataFeature => ({ + name, + source, +}); diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts new file mode 100644 index 000000000000..58b3beab4288 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.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 { + InfraBackendFrameworkAdapter, + InfraFrameworkRequest, + InfraMetadataAggregationBucket, + InfraMetadataAggregationResponse, +} from '../../../lib/adapters/framework'; +import { InfraSourceConfiguration } from '../../../lib/sources'; +import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; + +export interface InfraCloudMetricsAdapterResponse { + buckets: InfraMetadataAggregationBucket[]; +} + +export const getCloudMetricsMetadata = async ( + framework: InfraBackendFrameworkAdapter, + req: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + instanceId: string +): Promise => { + const metricQuery = { + allowNoIndices: true, + ignoreUnavailable: true, + index: sourceConfiguration.metricAlias, + body: { + query: { + bool: { + filter: [{ match: { 'cloud.instance.id': instanceId } }], + should: CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })), + }, + }, + size: 0, + aggs: { + metrics: { + terms: { + field: 'event.dataset', + size: 1000, + }, + }, + }, + }, + }; + + const response = await framework.callWithRequest< + {}, + { + metrics?: InfraMetadataAggregationResponse; + } + >(req, 'search', metricQuery); + + const buckets = + response.aggregations && response.aggregations.metrics + ? response.aggregations.metrics.buckets + : []; + + return { buckets }; +}; diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts new file mode 100644 index 000000000000..5f6bdd30fa2b --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraSourceConfiguration } from '../../../lib/sources'; + +export const getIdFieldName = (sourceConfiguration: InfraSourceConfiguration, nodeType: string) => { + switch (nodeType) { + case 'host': + return sourceConfiguration.fields.host; + case 'container': + return sourceConfiguration.fields.container; + default: + return sourceConfiguration.fields.pod; + } +}; diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts new file mode 100644 index 000000000000..812bc27fffc8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { + InfraFrameworkRequest, + InfraMetadataAggregationBucket, + InfraBackendFrameworkAdapter, + InfraMetadataAggregationResponse, +} from '../../../lib/adapters/framework'; +import { InfraSourceConfiguration } from '../../../lib/sources'; +import { getIdFieldName } from './get_id_field_name'; +import { NAME_FIELDS } from '../../../lib/constants'; + +export interface InfraMetricsAdapterResponse { + id: string; + name?: string; + buckets: InfraMetadataAggregationBucket[]; +} + +export const getMetricMetadata = async ( + framework: InfraBackendFrameworkAdapter, + req: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + nodeId: string, + nodeType: 'host' | 'pod' | 'container' +): Promise => { + const idFieldName = getIdFieldName(sourceConfiguration, nodeType); + + const metricQuery = { + allowNoIndices: true, + ignoreUnavailable: true, + index: sourceConfiguration.metricAlias, + body: { + query: { + bool: { + must_not: [{ match: { 'event.dataset': 'aws.ec2' } }], + filter: [ + { + match: { [idFieldName]: nodeId }, + }, + ], + }, + }, + size: 0, + aggs: { + nodeName: { + terms: { + field: NAME_FIELDS[nodeType], + size: 1, + }, + }, + metrics: { + terms: { + field: 'event.dataset', + size: 1000, + }, + }, + }, + }, + }; + + const response = await framework.callWithRequest< + {}, + { + metrics?: InfraMetadataAggregationResponse; + nodeName?: InfraMetadataAggregationResponse; + } + >(req, 'search', metricQuery); + + const buckets = + response.aggregations && response.aggregations.metrics + ? response.aggregations.metrics.buckets + : []; + + return { + id: nodeId, + name: get(response, ['aggregations', 'nodeName', 'buckets', 0, 'key'], nodeId), + buckets, + }; +}; diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts new file mode 100644 index 000000000000..5af25515a42e --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { first } from 'lodash'; +import { + InfraFrameworkRequest, + InfraBackendFrameworkAdapter, +} from '../../../lib/adapters/framework'; +import { InfraSourceConfiguration } from '../../../lib/sources'; +import { InfraNodeType } from '../../../graphql/types'; +import { InfraMetadataInfo } from '../../../../common/http_api/metadata_api'; +import { getPodNodeName } from './get_pod_node_name'; +import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; +import { getIdFieldName } from './get_id_field_name'; + +export const getNodeInfo = async ( + framework: InfraBackendFrameworkAdapter, + req: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + nodeId: string, + nodeType: 'host' | 'pod' | 'container' +): Promise => { + // If the nodeType is a Kubernetes pod then we need to get the node info + // from a host record instead of a pod. This is due to the fact that any host + // can report pod details and we can't rely on the host/cloud information associated + // with the kubernetes.pod.uid. We need to first lookup the `kubernetes.node.name` + // then use that to lookup the host's node information. + if (nodeType === InfraNodeType.pod) { + const kubernetesNodeName = await getPodNodeName( + framework, + req, + sourceConfiguration, + nodeId, + nodeType + ); + if (kubernetesNodeName) { + return getNodeInfo( + framework, + req, + sourceConfiguration, + kubernetesNodeName, + InfraNodeType.host + ); + } + return {}; + } + const params = { + allowNoIndices: true, + ignoreUnavailable: true, + terminateAfter: 1, + index: sourceConfiguration.metricAlias, + body: { + size: 1, + _source: ['host.*', 'cloud.*'], + query: { + bool: { + must_not: CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })), + filter: [{ match: { [getIdFieldName(sourceConfiguration, nodeType)]: nodeId } }], + }, + }, + }, + }; + const response = await framework.callWithRequest<{ _source: InfraMetadataInfo }, {}>( + req, + 'search', + params + ); + const firstHit = first(response.hits.hits); + if (firstHit) { + return firstHit._source; + } + return {}; +}; diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts new file mode 100644 index 000000000000..893707a4660e --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { first, get } from 'lodash'; +import { + InfraFrameworkRequest, + InfraBackendFrameworkAdapter, +} from '../../../lib/adapters/framework'; +import { InfraSourceConfiguration } from '../../../lib/sources'; +import { getIdFieldName } from './get_id_field_name'; + +export const getPodNodeName = async ( + framework: InfraBackendFrameworkAdapter, + req: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + nodeId: string, + nodeType: 'host' | 'pod' | 'container' +): Promise => { + const params = { + allowNoIndices: true, + ignoreUnavailable: true, + terminateAfter: 1, + index: sourceConfiguration.metricAlias, + body: { + size: 1, + _source: ['kubernetes.node.name'], + query: { + bool: { + filter: [ + { match: { [getIdFieldName(sourceConfiguration, nodeType)]: nodeId } }, + { exists: { field: `kubernetes.node.name` } }, + ], + }, + }, + }, + }; + const response = await framework.callWithRequest< + { _source: { kubernetes: { node: { name: string } } } }, + {} + >(req, 'search', params); + const firstHit = first(response.hits.hits); + if (firstHit) { + return get(firstHit, '_source.kubernetes.node.name'); + } +}; diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/pick_feature_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/pick_feature_name.ts new file mode 100644 index 000000000000..8b6bb49d9f64 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/pick_feature_name.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 { InfraMetadataAggregationBucket } from '../../../lib/adapters/framework'; + +export const pickFeatureName = (buckets: InfraMetadataAggregationBucket[]): string[] => { + if (buckets) { + const metadata = buckets.map(bucket => bucket.key); + return metadata; + } else { + return []; + } +}; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js index de908491eee6..d19fb4746882 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { getSuggestionsProvider } from '../field'; import indexPatternResponse from '../__fixtures__/index_pattern_response.json'; -import { isFilterable } from 'ui/index_patterns/static_utils'; +import { isFilterable } from 'ui/index_patterns'; describe('Kuery field suggestions', function () { let indexPattern; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js index 60c4e5ffc412..4f62c6344e9a 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js @@ -7,7 +7,7 @@ import React from 'react'; import { flatten } from 'lodash'; import { escapeKuery } from './escape_kuery'; import { sortPrefixFirst } from 'ui/utils/sort_prefix_first'; -import { isFilterable } from 'ui/index_patterns/static_utils'; +import { isFilterable } from 'ui/index_patterns'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap index 9045f3b44a7f..e82685d62a31 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.js.snap @@ -860,6 +860,7 @@ exports[`UploadLicense should display a modal when license requires acknowledgem >
    - + + +
    @@ -1314,6 +1317,7 @@ exports[`UploadLicense should display an error when ES says license is expired 1 >
    - + + +
    @@ -1772,6 +1778,7 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 >
    - + + +
    @@ -2230,6 +2239,7 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] >
    - + + +
    @@ -2684,6 +2696,7 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` >
    - + + +
    diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index 0856710adbba..d7f7e353799d 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export const EMS_DATA_FILE_PATH = 'ems/file'; -export const EMS_DATA_TMS_PATH = 'ems/tms'; -export const EMS_META_PATH = 'ems/meta'; -export const SPRITE_PATH = '/maps/sprite'; -export const MAKI_SPRITE_PATH = `${SPRITE_PATH}/maki`; +export const EMS_CATALOGUE_PATH = 'ems/catalogue'; +export const EMS_FILES_CATALOGUE_PATH = 'ems/files'; +export const EMS_FILES_DEFAULT_JSON_PATH = 'ems/files/file'; + +export const EMS_TILES_CATALOGUE_PATH = 'ems/tiles'; +export const EMS_TILES_RASTER_TILE_PATH = 'ems/tiles/raster/tile'; +export const EMS_TILES_RASTER_STYLE_PATH = 'ems/tiles/raster/style'; export const MAP_SAVED_OBJECT_TYPE = 'map'; export const APP_ID = 'maps'; @@ -37,6 +39,8 @@ export const ES_SIZE_LIMIT = 10000; export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn__isvisible__'; +export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; + export const ES_GEO_FIELD_TYPE = { GEO_POINT: 'geo_point', GEO_SHAPE: 'geo_shape' diff --git a/x-pack/legacy/plugins/maps/common/ems_util.js b/x-pack/legacy/plugins/maps/common/ems_util.js deleted file mode 100644 index 138c82f93a95..000000000000 --- a/x-pack/legacy/plugins/maps/common/ems_util.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 { EMS_DATA_FILE_PATH, EMS_DATA_TMS_PATH, GIS_API_PATH } from './constants'; - -export async function getEMSResources(emsClient, includeElasticMapsService, licenseUid, useRelativePathToProxy) { - - if (!includeElasticMapsService) { - return { - fileLayers: [], - tmsServices: [] - }; - } - - emsClient.addQueryParams({ license: licenseUid }); - const fileLayerObjs = await emsClient.getFileLayers(); - const tmsServicesObjs = await emsClient.getTMSServices(); - - const fileLayers = fileLayerObjs.map(fileLayer => { - //backfill to static settings - const format = fileLayer.getDefaultFormatType(); - const meta = fileLayer.getDefaultFormatMeta(); - - return { - name: fileLayer.getDisplayName(), - origin: fileLayer.getOrigin(), - id: fileLayer.getId(), - created_at: fileLayer.getCreatedAt(), - attribution: fileLayer.getHTMLAttribution(), - attributions: fileLayer.getAttributions(), - fields: fileLayer.getFieldsInLanguage(), - // eslint-disable-next-line max-len - url: useRelativePathToProxy ? `../${GIS_API_PATH}/${EMS_DATA_FILE_PATH}?id=${encodeURIComponent(fileLayer.getId())}` : fileLayer.getDefaultFormatUrl(), - format: format, //legacy: format and meta are split up - meta: meta, //legacy, format and meta are split up, - emsLink: fileLayer.getEMSHotLink() - }; - }); - - const tmsServices = await Promise.all(tmsServicesObjs.map(async tmsService => { - return { - name: tmsService.getDisplayName(), - origin: tmsService.getOrigin(), - id: tmsService.getId(), - minZoom: await tmsService.getMinZoom(), - maxZoom: await tmsService.getMaxZoom(), - attribution: tmsService.getHTMLAttribution(), - attributionMarkdown: tmsService.getMarkdownAttribution(), - // eslint-disable-next-line max-len - url: useRelativePathToProxy ? `../${GIS_API_PATH}/${EMS_DATA_TMS_PATH}?id=${encodeURIComponent(tmsService.getId())}&x={x}&y={y}&z={z}` : await tmsService.getUrlTemplate() - }; - })); - - return { fileLayers, tmsServices }; -} diff --git a/x-pack/legacy/plugins/maps/common/ems_util.test.js b/x-pack/legacy/plugins/maps/common/ems_util.test.js deleted file mode 100644 index 829e5f136a02..000000000000 --- a/x-pack/legacy/plugins/maps/common/ems_util.test.js +++ /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 { - getEMSResources -} from './ems_util'; - -// eslint-disable-next-line import/no-unresolved -import { getEMSClient } from 'ui/vis/__tests__/map/ems_client_util.js'; - -describe('ems util test', () => { - - - it('Should get relative paths when using proxy', async () => { - - const emsClient = getEMSClient({}); - const isEmsEnabled = true; - const licenseId = 'foobar'; - const useProxy = true; - const resources = await getEMSResources(emsClient, isEmsEnabled, licenseId, useProxy); - - expect(resources.tmsServices[0].url.startsWith('../api/maps/ems/tms')).toBe(true); - expect(resources.fileLayers[0].url.startsWith('../api/maps/ems/file')).toBe(true); - expect(resources.fileLayers[1].url.startsWith('../api/maps/ems/file')).toBe(true); - - }); - - - it('Should get absolute paths when not using proxy', async () => { - - const emsClient = getEMSClient({}); - const isEmsEnabled = true; - const licenseId = 'foobar'; - const useProxy = false; - const resources = await getEMSResources(emsClient, isEmsEnabled, licenseId, useProxy); - - expect(resources.tmsServices[0].url.startsWith('https://raster-style.foobar/')).toBe(true); - expect(resources.fileLayers[0].url.startsWith('https://vector-staging.maps.elastic.co/files')).toBe(true); - expect(resources.fileLayers[1].url.startsWith('https://vector-staging.maps.elastic.co/files')).toBe(true); - }); - - it('Should get empty response when ems is disabled', async () => { - - const emsClient = getEMSClient({}); - const isEmsEnabled = false; - const licenseId = 'foobar'; - const useProxy = true; - const resources = await getEMSResources(emsClient, isEmsEnabled, licenseId, !useProxy); - - expect(resources.tmsServices.length).toBe(0); - expect(resources.fileLayers.length).toBe(0); - - }); - - -}); diff --git a/x-pack/legacy/plugins/maps/public/angular/map.html b/x-pack/legacy/plugins/maps/public/angular/map.html index 0eefcc7da9cd..a0a10a97ae7e 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map.html +++ b/x-pack/legacy/plugins/maps/public/angular/map.html @@ -1,15 +1,17 @@
    - { @@ -63,34 +64,48 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta const savedMap = $route.current.locals.map; let unsubscribe; let initialLayerListConfig; - + const $state = new AppState(); const store = createMapStore(); + function getAppStateFilters() { + return _.get($state, 'filters', []); + } + $scope.$listen(globalState, 'fetch_with_changes', (diff) => { - if (diff.includes('time')) { - $scope.updateQueryAndDispatch({ query: $scope.query, dateRange: globalState.time }); + if (diff.includes('time') || diff.includes('filters')) { + onQueryChange({ + filters: [...globalState.filters, ...getAppStateFilters()], + time: globalState.time, + }); } if (diff.includes('refreshInterval')) { $scope.onRefreshChange({ isPaused: globalState.pause, refreshInterval: globalState.value }); } }); - const $state = new AppState(); $scope.$listen($state, 'fetch_with_changes', function (diff) { - if (diff.includes('query') && $state.query) { - $scope.updateQueryAndDispatch({ query: $state.query, dateRange: $scope.time }); + if ((diff.includes('query') || diff.includes('filters')) && $state.query) { + onQueryChange({ + filters: [...globalState.filters, ...getAppStateFilters()], + query: $state.query, + }); } }); function syncAppAndGlobalState() { $scope.$evalAsync(() => { + // appState $state.query = $scope.query; + $state.filters = data.filter.filterManager.getAppFilters(); $state.save(); + + // globalState globalState.time = $scope.time; globalState.refreshInterval = { pause: $scope.refreshConfig.isPaused, value: $scope.refreshConfig.interval, }; + globalState.filters = data.filter.filterManager.getGlobalFilters(); globalState.save(); }); } @@ -108,15 +123,41 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta mapStateJSON: savedMap.mapStateJSON, globalState: globalState, }); - syncAppAndGlobalState(); - $scope.indexPatterns = []; - $scope.updateQueryAndDispatch = function ({ dateRange, query }) { - $scope.query = query; - $scope.time = dateRange; + async function onQueryChange({ filters, query, time }) { + if (filters) { + await data.filter.filterManager.setFilters(filters); // Maps and merges filters + $scope.filters = data.filter.filterManager.getFilters(); + } + if (query) { + $scope.query = query; + } + if (time) { + $scope.time = time; + } syncAppAndGlobalState(); + dispatchSetQuery(); + } + + function dispatchSetQuery() { + store.dispatch(setQuery({ + filters: $scope.filters, + query: $scope.query, + timeFilters: $scope.time + })); + } - store.dispatch(setQuery({ query: $scope.query, timeFilters: $scope.time })); + $scope.indexPatterns = []; + $scope.updateQueryAndDispatch = function ({ dateRange, query }) { + onQueryChange({ + query, + time: dateRange, + }); + }; + $scope.updateFiltersAndDispatch = function (filters) { + onQueryChange({ + filters, + }); }; $scope.onRefreshChange = function ({ isPaused, refreshInterval }) { $scope.refreshConfig = { @@ -128,6 +169,12 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta store.dispatch(setRefreshConfig($scope.refreshConfig)); }; + function addFilters(newFilters) { + newFilters.forEach(filter => { + filter.$state = FilterStateStore.APP_STATE; + }); + $scope.updateFiltersAndDispatch([...$scope.filters, ...newFilters]); + } function hasUnsavedChanges() { @@ -158,7 +205,7 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta } window.addEventListener('beforeunload', beforeUnload); - function renderMap() { + async function renderMap() { // clear old UI state store.dispatch(setSelectedLayer(null)); store.dispatch(updateFlyout(FLYOUT_STATE.NONE)); @@ -170,6 +217,7 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta }); // sync store with savedMap mapState + let savedObjectFilters = []; if (savedMap.mapStateJSON) { const mapState = JSON.parse(savedMap.mapStateJSON); store.dispatch(setGotoWithCenter({ @@ -177,6 +225,9 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta lon: mapState.center.lon, zoom: mapState.zoom, })); + if (mapState.filters) { + savedObjectFilters = mapState.filters; + } } if (savedMap.uiStateJSON) { @@ -189,13 +240,19 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta initialLayerListConfig = copyPersistentState(layerList); store.dispatch(replaceLayerList(layerList)); store.dispatch(setRefreshConfig($scope.refreshConfig)); - store.dispatch(setQuery({ query: $scope.query, timeFilters: $scope.time })); + + const initialFilters = [ + ..._.get(globalState, 'filters', []), + ...getAppStateFilters(), + ...savedObjectFilters + ]; + await onQueryChange({ filters: initialFilters }); const root = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); render( - + , root diff --git a/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js b/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js index 0e9006d26aae..b1dc66d36add 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js +++ b/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js @@ -16,6 +16,7 @@ import { getMapExtent, getRefreshConfig, getQuery, + getFilters, } from '../../selectors/map_selectors'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors'; import { convertMapExtentToPolygon } from '../../elasticsearch_geo_utils'; @@ -102,6 +103,7 @@ module.factory('SavedGisMap', function (Private) { timeFilters: getTimeFilters(state), refreshConfig: getRefreshConfig(state), query: _.omit(getQuery(state), 'queryLastTriggeredAt'), + filters: getFilters(state), }); this.uiStateJSON = JSON.stringify({ diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js index d2fab0b86288..510adbfc035e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js @@ -22,6 +22,7 @@ export const FlyoutFooter = ({ const nextButton = showNextButton ? ( { + _viewLayer = async (source, options = {}) => { if (!source) { this.setState({ layer: null }); this.props.removeTransientLayer(); return; } - - const layerOptions = this.state.layer - ? { style: this.state.layer.getCurrentStyle().getDescriptor() } - : {}; - const newLayer = source.createDefaultLayer(layerOptions, this.props.mapColors); - this.setState({ layer: newLayer }, () => - this.props.viewLayer(this.state.layer)); + const layerInitProps = { + ...options, + ...(this.state.layer && { style: this.state.layer.getCurrentStyle().getDescriptor() }) + }; + const newLayer = source.createDefaultLayer(layerInitProps, this.props.mapColors); + this.setState( + { layer: newLayer }, + () => this.props.viewLayer(this.state.layer) + ); }; _clearLayerData = ({ keepSourceType = false }) => { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 1e3617ff076a..ad74b5738935 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -22,8 +22,7 @@ import { i18n } from '@kbn/i18n'; import { indexPatternService } from '../../../kibana_services'; import { Storage } from 'ui/storage'; -import { data } from 'plugins/data/setup'; -const { QueryBar } = data.query.ui; +import { QueryBar } from 'plugins/data'; const settings = chrome.getUiSettingsClient(); const localStorage = new Storage(window.localStorage); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index 1a15c4be4d34..994522b9086d 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -16,7 +16,7 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IndexPatternSelect } from 'ui/index_patterns/components/index_pattern_select'; +import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../../components/single_field_select'; import { FormattedMessage } from '@kbn/i18n/react'; import { getTermsFields } from '../../../../index_pattern_util'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js index 13cb77948f38..deafb561f118 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js @@ -14,8 +14,7 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { data } from 'plugins/data/setup'; -const { QueryBar } = data.query.ui; +import { QueryBar } from 'plugins/data'; import { Storage } from 'ui/storage'; const settings = chrome.getUiSettingsClient(); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/image_utils.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/image_utils.js new file mode 100644 index 000000000000..eae2d9fd314f --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/image_utils.js @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* @notice + * This product includes code that is adapted from mapbox-gl-js, which is + * available under a "BSD-3-Clause" license. + * https://github.com/mapbox/mapbox-gl-js/blob/master/src/util/image.js + * + * Copyright (c) 2016, Mapbox + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Mapbox GL JS nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import assert from 'assert'; + +function createImage(image, { width, height }, channels, data) { + if (!data) { + data = new Uint8Array(width * height * channels); + } else if (data instanceof Uint8ClampedArray) { + data = new Uint8Array(data.buffer); + } else if (data.length !== width * height * channels) { + throw new RangeError('mismatched image size'); + } + image.width = width; + image.height = height; + image.data = data; + return image; +} + +function resizeImage(image, { width, height }, channels) { + if (width === image.width && height === image.height) { + return; + } + + const newImage = createImage({}, { width, height }, channels); + + copyImage(image, newImage, { x: 0, y: 0 }, { x: 0, y: 0 }, { + width: Math.min(image.width, width), + height: Math.min(image.height, height) + }, channels); + + image.width = width; + image.height = height; + image.data = newImage.data; +} + +function copyImage(srcImg, dstImg, srcPt, dstPt, size, channels) { + if (size.width === 0 || size.height === 0) { + return dstImg; + } + + if (size.width > srcImg.width || + size.height > srcImg.height || + srcPt.x > srcImg.width - size.width || + srcPt.y > srcImg.height - size.height) { + throw new RangeError('out of range source coordinates for image copy'); + } + + if (size.width > dstImg.width || + size.height > dstImg.height || + dstPt.x > dstImg.width - size.width || + dstPt.y > dstImg.height - size.height) { + throw new RangeError('out of range destination coordinates for image copy'); + } + + const srcData = srcImg.data; + const dstData = dstImg.data; + + assert(srcData !== dstData); + + for (let y = 0; y < size.height; y++) { + const srcOffset = ((srcPt.y + y) * srcImg.width + srcPt.x) * channels; + const dstOffset = ((dstPt.y + y) * dstImg.width + dstPt.x) * channels; + for (let i = 0; i < size.width * channels; i++) { + dstData[dstOffset + i] = srcData[srcOffset + i]; + } + } + + return dstImg; +} + +export class AlphaImage { + + constructor(size, data) { + createImage(this, size, 1, data); + } + + resize(size) { + resizeImage(this, size, 1); + } + + clone() { + return new AlphaImage({ width: this.width, height: this.height }, new Uint8Array(this.data)); + } + + static copy(srcImg, dstImg, srcPt, dstPt, size) { + copyImage(srcImg, dstImg, srcPt, dstPt, size, 1); + } +} + +// Not premultiplied, because ImageData is not premultiplied. +// UNPACK_PREMULTIPLY_ALPHA_WEBGL must be used when uploading to a texture. +export class RGBAImage { + + // data must be a Uint8Array instead of Uint8ClampedArray because texImage2D does not + // support Uint8ClampedArray in all browsers + + constructor(size, data) { + createImage(this, size, 4, data); + } + + resize(size) { + resizeImage(this, size, 4); + } + + replace(data, copy) { + if (copy) { + this.data.set(data); + } else if (data instanceof Uint8ClampedArray) { + this.data = new Uint8Array(data.buffer); + } else { + this.data = data; + } + } + + clone() { + return new RGBAImage({ width: this.width, height: this.height }, new Uint8Array(this.data)); + } + + static copy(srcImg, dstImg, srcPt, dstPt, size) { + copyImage(srcImg, dstImg, srcPt, dstPt, size, 4); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/mb.utils.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/mb.utils.test.js new file mode 100644 index 000000000000..d38dc1cd6de5 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/mb.utils.test.js @@ -0,0 +1,327 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { removeOrphanedSourcesAndLayers, syncLayerOrderForSingleLayer } from './utils'; +import _ from 'lodash'; + +class MockMbMap { + + constructor(style) { + this._style = _.cloneDeep(style); + } + + getStyle() { + return _.cloneDeep(this._style); + } + + moveLayer(mbLayerId, nextMbLayerId) { + + const indexOfLayerToMove = this._style.layers.findIndex(layer => { + return layer.id === mbLayerId; + }); + + const layerToMove = this._style.layers[indexOfLayerToMove]; + this._style.layers.splice(indexOfLayerToMove, 1); + + const indexOfNextLayer = this._style.layers.findIndex(layer => { + return layer.id === nextMbLayerId; + }); + + this._style.layers.splice(indexOfNextLayer, 0, layerToMove); + + } + + removeSource(sourceId) { + delete this._style.sources[sourceId]; + } + + removeLayer(layerId) { + const layerToRemove = this._style.layers.findIndex(layer => { + return layer.id === layerId; + }); + this._style.layers.splice(layerToRemove, 1); + } +} + + +class MockLayer { + constructor(layerId, mbSourceIds, mbLayerIdsToSource) { + this._mbSourceIds = mbSourceIds; + this._mbLayerIdsToSource = mbLayerIdsToSource; + this._layerId = layerId; + } + getId() { + return this._layerId; + } + getMbSourceIds() { + return this._mbSourceIds; + } + getMbLayersIdsToSource() { + return this._mbLayerIdsToSource; + } + + getMbLayerIds() { + return this._mbLayerIdsToSource.map(({ id }) => id); + } + + ownsMbLayerId(mbLayerId) { + return this._mbLayerIdsToSource.some(mbLayerToSource => { + return mbLayerToSource.id === mbLayerId; + }); + } + + ownsMbSourceId(mbSourceId) { + return this._mbSourceIds.some(id => mbSourceId === id); + } + +} + + +function getMockStyle(orderedMockLayerList) { + + const mockStyle = { + sources: {}, + layers: [] + }; + + orderedMockLayerList.forEach(mockLayer => { + mockLayer.getMbSourceIds().forEach((mbSourceId) => { + mockStyle.sources[mbSourceId] = {}; + }); + mockLayer.getMbLayersIdsToSource().forEach(({ id, source }) => { + mockStyle.layers.push({ + id: id, + source: source + }); + }); + }); + + return mockStyle; +} + + +function makeSingleSourceMockLayer(layerId) { + return new MockLayer( + layerId, + [layerId], + [{ id: layerId + '_fill', source: layerId }, { id: layerId + '_line', source: layerId }] + ); +} + +function makeMultiSourceMockLayer(layerId) { + const source1 = layerId + '_source1'; + const source2 = layerId + '_source2'; + return new MockLayer( + layerId, + [source1, source2], + [ + { id: source1 + '_fill', source: source1 }, + { id: source2 + '_line', source: source2 }, + { id: source1 + '_line', source: source1 }, + { id: source1 + '_point', source: source1 } + ] + ); +} + +describe('mb/utils', () => { + + test('should remove foo and bar layer', async () => { + + const bazLayer = makeSingleSourceMockLayer('baz'); + const fooLayer = makeSingleSourceMockLayer('foo'); + const barLayer = makeSingleSourceMockLayer('bar'); + + const currentLayerList = [bazLayer, fooLayer, barLayer]; + const nextLayerList = [bazLayer]; + + const currentStyle = getMockStyle(currentLayerList); + const mockMbMap = new MockMbMap(currentStyle); + + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + const removedStyle = mockMbMap.getStyle(); + + + const nextStyle = getMockStyle(nextLayerList); + expect(removedStyle).toEqual(nextStyle); + + }); + + + test('should remove foo and bar layer (multisource)', async () => { + + const bazLayer = makeMultiSourceMockLayer('baz'); + const fooLayer = makeMultiSourceMockLayer('foo'); + const barLayer = makeMultiSourceMockLayer('bar'); + + const currentLayerList = [bazLayer, fooLayer, barLayer]; + const nextLayerList = [bazLayer]; + + const currentStyle = getMockStyle(currentLayerList); + const mockMbMap = new MockMbMap(currentStyle); + + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + const removedStyle = mockMbMap.getStyle(); + + + const nextStyle = getMockStyle(nextLayerList); + expect(removedStyle).toEqual(nextStyle); + + }); + + test('should not remove anything', async () => { + + const bazLayer = makeSingleSourceMockLayer('baz'); + const fooLayer = makeSingleSourceMockLayer('foo'); + const barLayer = makeSingleSourceMockLayer('bar'); + + const currentLayerList = [bazLayer, fooLayer, barLayer]; + const nextLayerList = [bazLayer, fooLayer, barLayer]; + + const currentStyle = getMockStyle(currentLayerList); + const mockMbMap = new MockMbMap(currentStyle); + + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + const removedStyle = mockMbMap.getStyle(); + + const nextStyle = getMockStyle(nextLayerList); + expect(removedStyle).toEqual(nextStyle); + + }); + + test('should move bar layer in front of foo layer', async () => { + + const fooLayer = makeSingleSourceMockLayer('foo'); + const barLayer = makeSingleSourceMockLayer('bar'); + + const currentLayerOrder = [fooLayer, barLayer]; + const nextLayerListOrder = [barLayer, fooLayer]; + + const currentStyle = getMockStyle(currentLayerOrder); + const mockMbMap = new MockMbMap(currentStyle); + syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); + const orderedStyle = mockMbMap.getStyle(); + + const nextStyle = getMockStyle(nextLayerListOrder); + expect(orderedStyle).toEqual(nextStyle); + + }); + + + + test('should fail at moving multiple layers (this tests a limitation of the sync)', async () => { + + //This is a known limitation of the layer order syncing. + //It assumes only a single layer will have moved. + //In practice, the Maps app will likely not cause multiple layers to move at once: + // - the UX only allows dragging a single layer + // - redux triggers a updates frequently enough + //But this is conceptually "wrong", as the sync does not actually operate in the same way as all the other mb-syncing methods + + const fooLayer = makeSingleSourceMockLayer('foo'); + const barLayer = makeSingleSourceMockLayer('bar'); + const foozLayer = makeSingleSourceMockLayer('foo'); + const bazLayer = makeSingleSourceMockLayer('baz'); + + const currentLayerOrder = [fooLayer, barLayer, foozLayer, bazLayer]; + const nextLayerListOrder = [bazLayer, barLayer, foozLayer, fooLayer]; + + const currentStyle = getMockStyle(currentLayerOrder); + const mockMbMap = new MockMbMap(currentStyle); + syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); + const orderedStyle = mockMbMap.getStyle(); + + const nextStyle = getMockStyle(nextLayerListOrder); + const isSyncSuccesful = _.isEqual(orderedStyle, nextStyle); + expect(isSyncSuccesful).toEqual(false); + + }); + + + test('should move bar layer in front of foo layer (multi source)', async () => { + + const fooLayer = makeSingleSourceMockLayer('foo'); + const barLayer = makeMultiSourceMockLayer('bar'); + + const currentLayerOrder = [fooLayer, barLayer]; + const nextLayerListOrder = [barLayer, fooLayer]; + + const currentStyle = getMockStyle(currentLayerOrder); + const mockMbMap = new MockMbMap(currentStyle); + syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); + const orderedStyle = mockMbMap.getStyle(); + + const nextStyle = getMockStyle(nextLayerListOrder); + expect(orderedStyle).toEqual(nextStyle); + + }); + + test('should move bar layer in front of foo layer, but after baz layer', async () => { + + + const bazLayer = makeSingleSourceMockLayer('baz'); + const fooLayer = makeSingleSourceMockLayer('foo'); + const barLayer = makeSingleSourceMockLayer('bar'); + + const currentLayerOrder = [bazLayer, fooLayer, barLayer]; + const nextLayerListOrder = [bazLayer, barLayer, fooLayer]; + + + const currentStyle = getMockStyle(currentLayerOrder); + const mockMbMap = new MockMbMap(currentStyle); + syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); + const orderedStyle = mockMbMap.getStyle(); + + const nextStyle = getMockStyle(nextLayerListOrder); + expect(orderedStyle).toEqual(nextStyle); + + }); + + test('should reorder foo and bar and remove baz', async () => { + + + const bazLayer = makeSingleSourceMockLayer('baz'); + const fooLayer = makeSingleSourceMockLayer('foo'); + const barLayer = makeSingleSourceMockLayer('bar'); + + const currentLayerOrder = [bazLayer, fooLayer, barLayer]; + const nextLayerListOrder = [barLayer, fooLayer]; + + + const currentStyle = getMockStyle(currentLayerOrder); + const mockMbMap = new MockMbMap(currentStyle); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder); + syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); + const orderedStyle = mockMbMap.getStyle(); + + const nextStyle = getMockStyle(nextLayerListOrder); + expect(orderedStyle).toEqual(nextStyle); + + }); + + test('should reorder foo and bar and remove baz, when having multi-source multi-layer data', async () => { + + + const bazLayer = makeMultiSourceMockLayer('baz'); + const fooLayer = makeSingleSourceMockLayer('foo'); + const barLayer = makeMultiSourceMockLayer('bar'); + + const currentLayerOrder = [bazLayer, fooLayer, barLayer]; + const nextLayerListOrder = [barLayer, fooLayer]; + + + const currentStyle = getMockStyle(currentLayerOrder); + const mockMbMap = new MockMbMap(currentStyle); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder); + syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); + const orderedStyle = mockMbMap.getStyle(); + + const nextStyle = getMockStyle(nextLayerListOrder); + expect(orderedStyle).toEqual(nextStyle); + + }); + + +}); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js index b32d5a9b9e62..6019b07fdddf 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js @@ -5,57 +5,20 @@ */ import _ from 'lodash'; -import mapboxgl from 'mapbox-gl'; -import chrome from 'ui/chrome'; -import { MAKI_SPRITE_PATH } from '../../../../common/constants'; - -function relativeToAbsolute(url) { - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} - -export async function createMbMapInstance({ node, initialView, scrollZoom }) { - const makiUrl = relativeToAbsolute(chrome.addBasePath(MAKI_SPRITE_PATH)); - return new Promise((resolve) => { - const options = { - attributionControl: false, - container: node, - style: { - version: 8, - sources: {}, - layers: [], - sprite: makiUrl - }, - scrollZoom, - preserveDrawingBuffer: chrome.getInjected('preserveDrawingBuffer', false) - }; - if (initialView) { - options.zoom = initialView.zoom; - options.center = { - lng: initialView.lon, - lat: initialView.lat - }; - } - const mbMap = new mapboxgl.Map(options); - mbMap.dragRotate.disable(); - mbMap.touchZoomRotate.disableRotation(); - mbMap.addControl( - new mapboxgl.NavigationControl({ showCompass: false }), 'top-left' - ); - mbMap.on('load', () => { - resolve(mbMap); - }); - }); -} +import { RGBAImage } from './image_utils'; export function removeOrphanedSourcesAndLayers(mbMap, layerList) { - const layerIds = layerList.map((layer) => layer.getId()); + const mbStyle = mbMap.getStyle(); const mbSourcesToRemove = []; for (const sourceId in mbStyle.sources) { - if (layerIds.indexOf(sourceId) === -1) { - mbSourcesToRemove.push(sourceId); + if (mbStyle.sources.hasOwnProperty(sourceId)) { + const layer = layerList.find(layer => { + return layer.ownsMbSourceId(sourceId); + }); + if (!layer) { + mbSourcesToRemove.push(sourceId); + } } } const mbLayersToRemove = []; @@ -73,32 +36,91 @@ export function removeOrphanedSourcesAndLayers(mbMap, layerList) { } -export function syncLayerOrder(mbMap, layerList) { - if (layerList && layerList.length) { - const mbLayers = mbMap.getStyle().layers.slice(); - const currentLayerOrder = _.uniq( // Consolidate layers and remove suffix - mbLayers.map(({ id }) => id.substring(0, id.lastIndexOf('_')))); - const newLayerOrder = layerList.map(l => l.getId()) - .filter(layerId => currentLayerOrder.includes(layerId)); - let netPos = 0; - let netNeg = 0; - const movementArr = currentLayerOrder.reduce((accu, id, idx) => { - const movement = newLayerOrder.findIndex(newOId => newOId === id) - idx; - movement > 0 ? netPos++ : movement < 0 && netNeg++; - accu.push({ id, movement }); - return accu; - }, []); - if (netPos === 0 && netNeg === 0) { return; } - const movedLayer = (netPos >= netNeg) && movementArr.find(l => l.movement < 0).id || +/** + * This is function assumes only a single layer moved in the layerList, compared to mbMap + * It is optimized to minimize the amount of mbMap.moveLayer calls. + * @param mbMap + * @param layerList + */ +export function syncLayerOrderForSingleLayer(mbMap, layerList) { + + if (!layerList || layerList.length === 0) { + return; + } + + + const mbLayers = mbMap.getStyle().layers.slice(); + const layerIds = mbLayers.map(mbLayer => { + const layer = layerList.find(layer => layer.ownsMbLayerId(mbLayer.id)); + return layer.getId(); + }); + + const currentLayerOrderLayerIds = _.uniq(layerIds); + + const newLayerOrderLayerIdsUnfiltered = layerList.map(l => l.getId()); + const newLayerOrderLayerIds = newLayerOrderLayerIdsUnfiltered.filter(layerId => currentLayerOrderLayerIds.includes(layerId)); + + let netPos = 0; + let netNeg = 0; + const movementArr = currentLayerOrderLayerIds.reduce((accu, id, idx) => { + const movement = newLayerOrderLayerIds.findIndex(newOId => newOId === id) - idx; + movement > 0 ? netPos++ : movement < 0 && netNeg++; + accu.push({ id, movement }); + return accu; + }, []); + if (netPos === 0 && netNeg === 0) { + return; + } + const movedLayerId = (netPos >= netNeg) && movementArr.find(l => l.movement < 0).id || (netPos < netNeg) && movementArr.find(l => l.movement > 0).id; - const nextLayerIdx = newLayerOrder.findIndex(layerId => layerId === movedLayer) + 1; - const nextLayerId = nextLayerIdx === newLayerOrder.length ? null : - mbLayers.find(({ id }) => id.startsWith(newLayerOrder[nextLayerIdx])).id; + const nextLayerIdx = newLayerOrderLayerIds.findIndex(layerId => layerId === movedLayerId) + 1; - mbLayers.forEach(({ id }) => { - if (id.startsWith(movedLayer)) { - mbMap.moveLayer(id, nextLayerId); - } + let nextMbLayerId; + if (nextLayerIdx === newLayerOrderLayerIds.length) { + nextMbLayerId = null; + } else { + const foundLayer = mbLayers.find(({ id: mbLayerId }) => { + const layerId = newLayerOrderLayerIds[nextLayerIdx]; + const layer = layerList.find(layer => layer.getId() === layerId); + return layer.ownsMbLayerId(mbLayerId); }); + nextMbLayerId = foundLayer.id; } + + const movedLayer = layerList.find(layer => layer.getId() === movedLayerId); + mbLayers.forEach(({ id: mbLayerId }) => { + if (movedLayer.ownsMbLayerId(mbLayerId)) { + mbMap.moveLayer(mbLayerId, nextMbLayerId); + } + }); + +} + +function getImageData(img) { + const canvas = window.document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) { + throw new Error('failed to create canvas 2d context'); + } + canvas.width = img.width; + canvas.height = img.height; + context.drawImage(img, 0, 0, img.width, img.height); + return context.getImageData(0, 0, img.width, img.height); +} + +export async function addSpritesheetToMap(json, img, mbMap) { + const image = new Image(); + image.onload = (el) => { + const imgData = getImageData(el.currentTarget); + for (const imageId in json) { + if (json.hasOwnProperty(imageId)) { + const { width, height, x, y, sdf, pixelRatio } = json[imageId]; + const data = new RGBAImage({ width, height }); + RGBAImage.copy(imgData, data, { x, y }, { x: 0, y: 0 }, { width, height }); + // TODO not sure how to catch errors? + mbMap.addImage(imageId, data, { pixelRatio, sdf }); + } + } + }; + image.src = img; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index befc0d82c26b..07d6d189437b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -8,7 +8,11 @@ import _ from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; import { ResizeChecker } from 'ui/resize_checker'; -import { syncLayerOrder, removeOrphanedSourcesAndLayers, createMbMapInstance } from './utils'; +import { + syncLayerOrderForSingleLayer, + removeOrphanedSourcesAndLayers, + addSpritesheetToMap +} from './utils'; import { DECIMAL_DEGREES_PRECISION, FEATURE_ID_PROPERTY_NAME, @@ -20,7 +24,12 @@ import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import { FeatureTooltip } from '../feature_tooltip'; import { DRAW_TYPE } from '../../../actions/map_actions'; import { createShapeFilterWithMeta, createExtentFilterWithMeta } from '../../../elasticsearch_geo_utils'; +import chrome from 'ui/chrome'; +import { spritesheet } from '@elastic/maki'; +import sprites1 from '@elastic/maki/dist/sprite@1.png'; +import sprites2 from '@elastic/maki/dist/sprite@2.png'; +const isRetina = window.devicePixelRatio === 2; const mbDrawModes = MapboxDraw.modes; mbDrawModes.draw_rectangle = DrawRectangle; @@ -340,13 +349,43 @@ export class MBMapContainer extends React.Component { this._mbDrawControl.changeMode(mbDrawMode); } + + async _createMbMapInstance() { + const initialView = this.props.goto ? this.props.goto.center : null; + return new Promise((resolve) => { + const options = { + attributionControl: false, + container: this.refs.mapContainer, + style: { + version: 8, + sources: {}, + layers: [] + }, + scrollZoom: this.props.scrollZoom, + preserveDrawingBuffer: chrome.getInjected('preserveDrawingBuffer', false) + }; + if (initialView) { + options.zoom = initialView.zoom; + options.center = { + lng: initialView.lon, + lat: initialView.lat + }; + } + const mbMap = new mapboxgl.Map(options); + mbMap.dragRotate.disable(); + mbMap.touchZoomRotate.disableRotation(); + mbMap.addControl( + new mapboxgl.NavigationControl({ showCompass: false }), 'top-left' + ); + mbMap.on('load', () => { + resolve(mbMap); + }); + }); + } + async _initializeMap() { try { - this._mbMap = await createMbMapInstance({ - node: this.refs.mapContainer, - initialView: this.props.goto ? this.props.goto.center : null, - scrollZoom: this.props.scrollZoom - }); + this._mbMap = await this._createMbMapInstance(); } catch(error) { this.props.setMapInitError(error.message); return; @@ -356,6 +395,8 @@ export class MBMapContainer extends React.Component { return; } + this._loadMakiSprites(); + this._initResizerChecker(); // moveend callback is debounced to avoid updating map extent state while map extent is still changing @@ -396,6 +437,12 @@ export class MBMapContainer extends React.Component { }); } + _loadMakiSprites() { + const sprites = isRetina ? sprites2 : sprites1; + const json = isRetina ? spritesheet[2] : spritesheet[1]; + addSpritesheetToMap(json, sprites, this._mbMap); + } + _hideTooltip() { if (this._mbPopup.isOpen()) { this._mbPopup.remove(); @@ -518,7 +565,7 @@ export class MBMapContainer extends React.Component { layer.syncLayerWithMB(this._mbMap); }); - syncLayerOrder(this._mbMap, this.props.layerList); + syncLayerOrderForSingleLayer(this._mbMap, this.props.layerList); }; _syncMbMapWithInspector = () => { diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js index d19d0c895143..dfffcb65279f 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +jest.mock('ui/new_platform'); + import { hitsToGeoJson, geoPointToGeometry, @@ -12,7 +14,7 @@ import { convertMapExtentToPolygon, } from './elasticsearch_geo_utils'; -import { flattenHitWrapper } from 'ui/index_patterns/_flatten_hit'; +import { flattenHitWrapper } from 'ui/index_patterns'; const geoFieldName = 'location'; const mapExtent = { diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js index da555d35d70c..e8ddee196e1f 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js @@ -15,6 +15,7 @@ import { Embeddable, executeTriggerActions } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; +import { onlyDisabledFiltersChanged } from '../../../../../../src/legacy/core_plugins/data/public'; import { I18nContext } from 'ui/i18n'; import { GisMap } from '../connected_components/gis_map'; @@ -65,7 +66,7 @@ export class MapEmbeddable extends Embeddable { onContainerStateChanged(containerState) { if (!_.isEqual(containerState.timeRange, this._prevTimeRange) || !_.isEqual(containerState.query, this._prevQuery) || - !_.isEqual(containerState.filters, this._prevFilters)) { + !onlyDisabledFiltersChanged(containerState.filters, this._prevFilters)) { this._dispatchSetQuery(containerState); } diff --git a/x-pack/legacy/plugins/maps/public/inspector/views/map_view.js b/x-pack/legacy/plugins/maps/public/inspector/views/map_view.js index e1bdcbdf191b..5be15e1edccb 100644 --- a/x-pack/legacy/plugins/maps/public/inspector/views/map_view.js +++ b/x-pack/legacy/plugins/maps/public/inspector/views/map_view.js @@ -6,8 +6,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; - -import { InspectorView } from 'ui/inspector'; import { MapDetails } from './map_details'; import { i18n } from '@kbn/i18n'; @@ -38,14 +36,12 @@ class MapViewComponent extends Component { render() { return ( - - - + ); } } diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 515e20cff936..23a1380181c7 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -8,7 +8,7 @@ import { uiModules } from 'ui/modules'; import { SearchSourceProvider } from 'ui/courier'; import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; export { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { data } from 'plugins/data/setup'; +import { setup as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; export const indexPatternService = data.indexPatterns.indexPatterns; diff --git a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js index d2b71aec8c8b..7ccd98b88996 100644 --- a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js @@ -37,13 +37,17 @@ export class HeatmapLayer extends VectorLayer { } _getHeatmapLayerId() { - return this.getId() + '_heatmap'; + return this.makeMbLayerId('heatmap'); } getMbLayerIds() { return [this._getHeatmapLayerId()]; } + ownsMbLayerId(mbLayerId) { + return this._getHeatmapLayerId() === mbLayerId; + } + syncLayerWithMB(mbMap) { super._syncSourceBindingWithMb(mbMap); diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index d4851a9c414e..f0186d91ab1a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -9,7 +9,7 @@ import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import turf from 'turf'; import turfBooleanContains from '@turf/boolean-contains'; import { DataRequest } from './util/data_request'; -import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; +import { MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; import uuid from 'uuid/v4'; import { copyPersistentState } from '../reducers/util'; import { i18n } from '@kbn/i18n'; @@ -73,6 +73,10 @@ export class AbstractLayer { return clonedDescriptor; } + makeMbLayerId(layerNameSuffix) { + return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; + } + isJoinable() { return this._source.isJoinable(); } @@ -264,6 +268,14 @@ export class AbstractLayer { throw new Error('Should implement AbstractLayer#getMbLayerIds'); } + ownsMbLayerId() { + throw new Error('Should implement AbstractLayer#ownsMbLayerId'); + } + + ownsMbSourceId() { + throw new Error('Should implement AbstractLayer#ownsMbSourceId'); + } + canShowTooltip() { return false; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/constants.js b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/constants.js new file mode 100644 index 000000000000..779ac2861e9f --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/constants.js @@ -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 const DEFAULT_APPLY_GLOBAL_QUERY = false; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index 34faf4b29d34..e750f1a33688 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -6,11 +6,18 @@ import { AbstractVectorSource } from '../vector_source'; import React from 'react'; -import { ES_GEO_FIELD_TYPE, GEOJSON_FILE } from '../../../../common/constants'; +import { + ES_GEO_FIELD_TYPE, + GEOJSON_FILE, + ES_SIZE_LIMIT +} from '../../../../common/constants'; import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; import { ESSearchSource } from '../es_search_source'; import uuid from 'uuid/v4'; import _ from 'lodash'; +import { + DEFAULT_APPLY_GLOBAL_QUERY +} from './constants'; export class GeojsonFileSource extends AbstractVectorSource { @@ -20,6 +27,9 @@ export class GeojsonFileSource extends AbstractVectorSource { static icon = 'importAction'; static isIndexingSource = true; static isBeta = true; + static layerDefaults = { + applyGlobalQuery: DEFAULT_APPLY_GLOBAL_QUERY + } static createDescriptor(geoJson, name) { // Wrap feature as feature collection if needed @@ -42,8 +52,12 @@ export class GeojsonFileSource extends AbstractVectorSource { ) => { return (indexResponses = {}) => { const { indexDataResp, indexPatternResp } = indexResponses; - if (!(indexDataResp && indexDataResp.success) || - !(indexPatternResp && indexPatternResp.success)) { + + const indexCreationFailed = !(indexDataResp && indexDataResp.success); + const allDocsFailed = indexDataResp.failures.length === indexDataResp.docCount; + const indexPatternCreationFailed = !(indexPatternResp && indexPatternResp.success); + + if (indexCreationFailed || allDocsFailed || indexPatternCreationFailed) { importErrorHandler(indexResponses); return; } @@ -56,12 +70,15 @@ export class GeojsonFileSource extends AbstractVectorSource { if (!indexPatternId || !geoField) { addAndViewSource(null); } else { + // Only turn on bounds filter for large doc counts + const filterByMapBounds = indexDataResp.docCount > ES_SIZE_LIMIT; const source = new ESSearchSource({ id: uuid(), indexPatternId, geoField, + filterByMapBounds }, inspectorAdapters); - addAndViewSource(source); + addAndViewSource(source, this.layerDefaults); importSuccessHandler(indexResponses); } }; @@ -107,8 +124,17 @@ export class GeojsonFileSource extends AbstractVectorSource { } async getGeoJsonWithMeta() { + const copiedPropsFeatures = this._descriptor.featureCollection.features + .map(feature => ({ + type: 'Feature', + geometry: feature.geometry, + properties: feature.properties ? { ...feature.properties } : {} + })); return { - data: this._descriptor.featureCollection, + data: { + type: 'FeatureCollection', + features: copiedPropsFeatures + }, meta: {} }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.js index 8eb6adb8768a..c892f2ce9573 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.js @@ -11,7 +11,7 @@ import { EuiFormRow, } from '@elastic/eui'; -import { getEmsVectorFilesMeta } from '../../../meta'; +import { getEMSClient } from '../../../meta'; import { getEmsUnavailableMessage } from '../ems_unavailable_message'; import { i18n } from '@kbn/i18n'; @@ -23,7 +23,14 @@ export class EMSFileCreateSourceEditor extends React.Component { }; _loadFileOptions = async () => { - const options = await getEmsVectorFilesMeta(); + const emsClient = getEMSClient(); + const fileLayers = await emsClient.getFileLayers(); + const options = fileLayers.map(fileLayer => { + return { + id: fileLayer.getId(), + name: fileLayer.getDisplayName() + }; + }); if (this._isMounted) { this.setState({ emsFileOptionsRaw: options diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index 06910a865696..e18fe0cea127 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -8,7 +8,7 @@ import { AbstractVectorSource } from '../vector_source'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import React from 'react'; import { EMS_FILE } from '../../../../common/constants'; -import { getEmsVectorFilesMeta } from '../../../meta'; +import { getEMSClient } from '../../../meta'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -57,10 +57,11 @@ export class EMSFileSource extends AbstractVectorSource { ); } - async _getEmsVectorFileMeta() { - const emsFiles = await getEmsVectorFilesMeta(); - const meta = emsFiles.find((source => source.id === this._descriptor.id)); - if (!meta) { + async _getEMSFileLayer() { + const emsClient = getEMSClient(); + const emsFileLayers = await emsClient.getFileLayers(); + const emsFileLayer = emsFileLayers.find((fileLayer => fileLayer.getId() === this._descriptor.id)); + if (!emsFileLayer) { throw new Error(i18n.translate('xpack.maps.source.emsFile.unableToFindIdErrorMessage', { defaultMessage: `Unable to find EMS vector shapes for id: {id}`, values: { @@ -68,15 +69,15 @@ export class EMSFileSource extends AbstractVectorSource { } })); } - return meta; + return emsFileLayer; } async getGeoJsonWithMeta() { - const emsVectorFileMeta = await this._getEmsVectorFileMeta(); + const emsFileLayer = await this._getEMSFileLayer(); const featureCollection = await AbstractVectorSource.getGeoJson({ - format: emsVectorFileMeta.format, + format: emsFileLayer.getDefaultFormatType(), featureCollectionPath: 'data', - fetchUrl: emsVectorFileMeta.url + fetchUrl: emsFileLayer.getDefaultFormatUrl() }); return { data: featureCollection, @@ -87,8 +88,8 @@ export class EMSFileSource extends AbstractVectorSource { async getImmutableProperties() { let emsLink; try { - const emsVectorFileMeta = await this._getEmsVectorFileMeta(); - emsLink = emsVectorFileMeta.emsLink; + const emsFileLayer = await this._getEMSFileLayer(); + emsLink = emsFileLayer.getEMSHotLink(); } catch(error) { // ignore error if EMS layer id could not be found } @@ -110,22 +111,23 @@ export class EMSFileSource extends AbstractVectorSource { async getDisplayName() { try { - const emsVectorFileMeta = await this._getEmsVectorFileMeta(); - return emsVectorFileMeta.name; + const emsFileLayer = await this._getEMSFileLayer(); + return emsFileLayer.getDisplayName(); } catch (error) { return this._descriptor.id; } } async getAttributions() { - const emsVectorFileMeta = await this._getEmsVectorFileMeta(); - return emsVectorFileMeta.attributions; + const emsFileLayer = await this._getEMSFileLayer(); + return emsFileLayer.getAttributions(); } async getLeftJoinFields() { - const emsVectorFileMeta = await this._getEmsVectorFileMeta(); - return emsVectorFileMeta.fields.map(f => { + const emsFileLayer = await this._getEMSFileLayer(); + const fields = emsFileLayer.getFieldsInLanguage(); + return fields.map(f => { return { name: f.name, label: f.description }; }); } @@ -135,14 +137,15 @@ export class EMSFileSource extends AbstractVectorSource { } async filterAndFormatPropertiesToHtml(properties) { - const meta = await this._getEmsVectorFileMeta(); + const emsFileLayer = await this._getEMSFileLayer(); const tooltipProperties = []; + const fields = emsFileLayer.getFieldsInLanguage(); for (const key in properties) { if (properties.hasOwnProperty(key) && this._descriptor.tooltipProperties.indexOf(key) > -1) { let newFieldName = key; - for (let i = 0; i < meta.fields.length; i++) { - if (meta.fields[i].name === key) { - newFieldName = meta.fields[i].description; + for (let i = 0; i < fields.length; i++) { + if (fields[i].name === key) { + newFieldName = fields[i].description; break; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js index 1100ee361572..43837a8b5d8a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js @@ -9,36 +9,31 @@ import { EMSFileSource } from './ems_file_source'; jest.mock('../../../kibana_services', () => {}); jest.mock('../../vector_layer', () => {}); -class MockEMSFileSource { - - constructor(emsFileSource) { - this._emsFileSource = emsFileSource; - this._emsFileSource._getEmsVectorFileMeta = () => { - return { - fields: [{ +function makeEMSFileSource(tooltipProperties) { + const emsFileSource = new EMSFileSource({ + tooltipProperties: tooltipProperties + }); + emsFileSource._getEMSFileLayer = () => { + return { + getFieldsInLanguage() { + return [{ name: 'iso2', description: 'ISO 2 CODE' - }] - }; + }]; + } }; - } - - async filterAndFormatPropertiesToHtml(props) { - return await this._emsFileSource.filterAndFormatPropertiesToHtml(props); - } - + }; + return emsFileSource; } + describe('EMS file source', () => { it('should create tooltip-properties with human readable label', async () => { - const emsFileSource = new EMSFileSource({ - tooltipProperties: ['iso2'] - }); - const mockEMSFileSource = new MockEMSFileSource(emsFileSource); + const mockEMSFileSource = makeEMSFileSource(['iso2']); const tooltipProperties = await mockEMSFileSource.filterAndFormatPropertiesToHtml({ 'iso2': 'US' }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js index 8ac8b9496964..9fbf80f0990f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js @@ -7,7 +7,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { TooltipSelector } from '../../../components/tooltip_selector'; -import { getEmsVectorFilesMeta } from '../../../meta'; +import { getEMSClient } from '../../../meta'; export class UpdateSourceEditor extends Component { @@ -32,9 +32,11 @@ export class UpdateSourceEditor extends Component { async loadFields() { let fields; try { - const emsFiles = await getEmsVectorFilesMeta(); - const meta = emsFiles.find((source => source.id === this.props.layerId)); - fields = meta.fields.map(field => { + const emsClient = getEMSClient(); + const emsFiles = await emsClient.getFileLayers(); + const emsFile = emsFiles.find((emsFile => emsFile.getId() === this.props.layerId)); + const emsFields = emsFile.getFieldsInLanguage(); + fields = emsFields.map(field => { return { type: 'string', name: field.name, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/create_source_editor.js index fa04bedb0d66..daa7d94c7665 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/create_source_editor.js @@ -11,7 +11,7 @@ import { EuiFormRow, } from '@elastic/eui'; -import { getEmsTMSServices } from '../../../meta'; +import { getEMSClient } from '../../../meta'; import { getEmsUnavailableMessage } from '../ems_unavailable_message'; import { i18n } from '@kbn/i18n'; @@ -24,7 +24,14 @@ export class EMSTMSCreateSourceEditor extends React.Component { }; _loadTmsOptions = async () => { - const options = await getEmsTMSServices(); + const emsClient = getEMSClient(); + const emsTMSServices = await emsClient.getTMSServices(); + const options = emsTMSServices.map(tmsService => { + return { + id: tmsService.getId(), + name: tmsService.getDisplayName() + }; + }); options.unshift({ id: AUTO_SELECT, name: i18n.translate('xpack.maps.source.emsTile.autoLabel', { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js index 57f0c2f29708..2c45deac47c9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js @@ -10,7 +10,7 @@ import React from 'react'; import { AbstractTMSSource } from '../tms_source'; import { TileLayer } from '../../tile_layer'; -import { getEmsTMSServices } from '../../../meta'; +import { getEMSClient } from '../../../meta'; import { EMSTMSCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -74,19 +74,18 @@ export class EMSTMSSource extends AbstractTMSSource { ]; } - async _getEmsTmsMeta() { - const emsTileServices = await getEmsTMSServices(); + async _getEMSTMSService() { + const emsClient = getEMSClient(); + const emsTMSServices = await emsClient.getTMSServices(); const emsTileLayerId = this._getEmsTileLayerId(); - const meta = emsTileServices.find(service => { - return service.id === emsTileLayerId; - }); - if (!meta) { + const tmsService = emsTMSServices.find(tmsService => tmsService.getId() === emsTileLayerId); + if (!tmsService) { throw new Error(i18n.translate('xpack.maps.source.emsTile.errorMessage', { defaultMessage: `Unable to find EMS tile configuration for id: {id}`, values: { id: emsTileLayerId } })); } - return meta; + return tmsService; } _createDefaultLayerDescriptor(options) { @@ -105,20 +104,21 @@ export class EMSTMSSource extends AbstractTMSSource { async getDisplayName() { try { - const emsTmsMeta = await this._getEmsTmsMeta(); - return emsTmsMeta.name; + const emsTMSService = await this._getEMSTMSService(); + return emsTMSService.getDisplayName(); } catch (error) { return this._getEmsTileLayerId(); } } async getAttributions() { - const emsTmsMeta = await this._getEmsTmsMeta(); - if (!emsTmsMeta.attributionMarkdown) { + const emsTMSService = await this._getEMSTMSService(); + const markdown = emsTMSService.getMarkdownAttribution(); + if (!markdown) { return []; } - return emsTmsMeta.attributionMarkdown.split('|').map((attribution) => { + return markdown.split('|').map((attribution) => { attribution = attribution.trim(); //this assumes attribution is plain markdown link const extractLink = /\[(.*)\]\((.*)\)/; @@ -131,8 +131,8 @@ export class EMSTMSSource extends AbstractTMSSource { } async getUrlTemplate() { - const emsTmsMeta = await this._getEmsTmsMeta(); - return emsTmsMeta.url; + const emsTMSService = await this._getEMSTMSService(); + return await emsTMSService.getUrlTemplate(); } _getEmsTileLayerId() { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.test.js index 6e594feeca51..e0d25d1d5722 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.test.js @@ -6,16 +6,33 @@ jest.mock('../../../meta', () => { return { - getEmsTMSServices: async () => { - return [ - { - id: 'road_map', - attributionMarkdown: '[foobar](http://foobar.org) | [foobaz](http://foobaz.org)' - }, { - id: 'satellite', - attributionMarkdown: '[satellite](http://satellite.org)' + getEMSClient: () => { + class MockTMSService { + constructor(config) { + this._config = config; } - ]; + getMarkdownAttribution() { + return this._config.attributionMarkdown; + } + getId() { + return this._config.id; + } + } + + return { + async getTMSServices() { + return [ + new MockTMSService({ + id: 'road_map', + attributionMarkdown: '[foobar](http://foobar.org) | [foobaz](http://foobaz.org)' + }), + new MockTMSService({ + id: 'satellite', + attributionMarkdown: '[satellite](http://satellite.org)' + }) + ]; + } + }; } }; }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js index f92bc6204b4c..d9c0f141c005 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; -import { IndexPatternSelect } from 'ui/index_patterns/components/index_pattern_select'; +import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { RENDER_AS } from './render_as'; import { indexPatternService } from '../../../kibana_services'; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js index 7b116988b6e6..61300ed209c1 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js @@ -9,7 +9,7 @@ import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow, EuiSpacer, EuiSwitch, EuiCallOut } from '@elastic/eui'; -import { IndexPatternSelect } from 'ui/index_patterns/components/index_pattern_select'; +import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { indexPatternService } from '../../../kibana_services'; import { NoIndexPatternCallout } from '../../../components/no_index_pattern_callout'; @@ -71,7 +71,7 @@ export class CreateSourceEditor extends Component { } }); return count; - } + }; debouncedLoad = _.debounce(async (indexPatternId) => { if (!indexPatternId || indexPatternId.length === 0) { @@ -143,11 +143,11 @@ export class CreateSourceEditor extends Component { ? { indexPatternId, geoField, filterByMapBounds } : null; this.props.onSourceConfigChange(sourceConfig); - } + }; _onNoIndexPatterns = () => { this.setState({ noGeoIndexPatternsExist: true }); - } + }; _renderGeoSelect() { if (!this.state.indexPattern) { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 1d52dcc147c5..d0cf7420d84a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -284,7 +284,7 @@ export class ESSearchSource extends AbstractESSource { searchSource.setField('size', 1); const query = { language: 'kuery', - query: `_id:${docId}` + query: `_id:"${docId}"` }; searchSource.setField('query', query); searchSource.setField('fields', this._descriptor.tooltipProperties); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/symbol_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/symbol_utils.js index 887b347b6ce9..a32ae8d414b4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/symbol_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/symbol_utils.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { maki } from '@kbn/maki'; +import maki from '@elastic/maki'; import xml2js from 'xml2js'; import { parseXmlString } from '../../../common/parse_xml_string'; @@ -70,31 +70,3 @@ export async function styleSvg(svgString, fill) { const builder = new xml2js.Builder(); return builder.buildObject(svgXml); } - -function addImageToMap(imageUrl, imageId, symbolId, mbMap) { - return new Promise((resolve, reject) => { - const img = new Image(LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE); - img.onload = () => { - mbMap.addImage(imageId, img); - resolve(); - }; - img.onerror = (err) => { - reject(err); - }; - img.src = imageUrl; - }); -} - -export async function loadImage(imageId, symbolId, color, mbMap) { - let symbolSvg; - try { - symbolSvg = getMakiSymbolSvg(symbolId); - } catch(error) { - return; - } - - const styledSvg = await styleSvg(symbolSvg, color); - const imageUrl = buildSrcUrl(styledSvg); - - await addImageToMap(imageUrl, imageId, symbolId, mbMap); -} diff --git a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js index ed4b2cb36d20..928a00546019 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js @@ -48,13 +48,21 @@ export class TileLayer extends AbstractLayer { } _getMbLayerId() { - return this.getId() + '_raster'; + return this.makeMbLayerId('raster'); } getMbLayerIds() { return [this._getMbLayerId()]; } + ownsMbLayerId(mbLayerId) { + return this._getMbLayerId() === mbLayerId; + } + + ownsMbSourceId(mbSourceId) { + return this.getId() === mbSourceId; + } + syncLayerWithMB(mbMap) { const source = mbMap.getSource(this.getId()); diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 829ce8ea599f..17146956af8d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -196,7 +196,12 @@ export class VectorLayer extends AbstractLayer { if (!featureCollection) { return null; } - const bbox = turf.bbox(featureCollection); + + const visibleFeatures = featureCollection.features.filter(feature => feature.properties[FEATURE_VISIBLE_PROPERTY_NAME]); + const bbox = turf.bbox({ + type: 'FeatureCollection', + features: visibleFeatures + }); return { min_lon: bbox[0], min_lat: bbox[1], @@ -206,11 +211,14 @@ export class VectorLayer extends AbstractLayer { } async getBounds(dataFilters) { - if (this._source.isBoundsAware()) { - const searchFilters = this._getSearchFilters(dataFilters); - return await this._source.getBoundsForFilters(searchFilters); + + const isStaticLayer = !this._source.isBoundsAware() || !this._source.isFilterByMapBounds(); + if (isStaticLayer) { + return this._getBoundsBasedOnData(); } - return this._getBoundsBasedOnData(); + + const searchFilters = this._getSearchFilters(dataFilters); + return await this._source.getBoundsForFilters(searchFilters); } async getLeftJoinFields() { @@ -676,25 +684,36 @@ export class VectorLayer extends AbstractLayer { } _getMbPointLayerId() { - return this.getId() + '_circle'; + return this.makeMbLayerId('circle'); } _getMbSymbolLayerId() { - return this.getId() + '_symbol'; + return this.makeMbLayerId('symbol'); } _getMbLineLayerId() { - return this.getId() + '_line'; + return this.makeMbLayerId('line'); } _getMbPolygonLayerId() { - return this.getId() + '_fill'; + return this.makeMbLayerId('fill'); } getMbLayerIds() { return [this._getMbPointLayerId(), this._getMbSymbolLayerId(), this._getMbLineLayerId(), this._getMbPolygonLayerId()]; } + ownsMbLayerId(mbLayerId) { + return this._getMbPointLayerId() === mbLayerId || + this._getMbLineLayerId() === mbLayerId || + this._getMbPolygonLayerId() === mbLayerId || + this._getMbSymbolLayerId() === mbLayerId; + } + + ownsMbSourceId(mbSourceId) { + return this.getId() === mbSourceId; + } + _addJoinsToSourceTooltips(tooltipsFromSource) { for (let i = 0; i < tooltipsFromSource.length; i++) { const tooltipProperty = tooltipsFromSource[i]; diff --git a/x-pack/legacy/plugins/maps/public/meta.js b/x-pack/legacy/plugins/maps/public/meta.js index b2dc55f3dc36..72e29b493a10 100644 --- a/x-pack/legacy/plugins/maps/public/meta.js +++ b/x-pack/legacy/plugins/maps/public/meta.js @@ -5,76 +5,76 @@ */ -import { GIS_API_PATH, EMS_META_PATH } from '../common/constants'; -import _ from 'lodash'; -import { getEMSResources } from '../common/ems_util'; +import { + GIS_API_PATH, + EMS_CATALOGUE_PATH +} from '../common/constants'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; import { EMSClient } from 'ui/vis/map/ems_client'; import { xpackInfo } from './kibana_services'; +import fetch from 'node-fetch'; const GIS_API_RELATIVE = `../${GIS_API_PATH}`; -let emsSources = null; -let loadingMetaPromise = null; +export function getKibanaRegionList() { + return chrome.getInjected('regionmapLayers'); +} -export async function getEMSDataSources() { - if (emsSources) { - return emsSources; - } +export function getKibanaTileMap() { + return chrome.getInjected('tilemap'); +} - if (loadingMetaPromise) { - return loadingMetaPromise; - } +function relativeToAbsolute(url) { + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} - loadingMetaPromise = new Promise(async (resolve, reject) => { - try { - const proxyElasticMapsServiceInMaps = chrome.getInjected('proxyElasticMapsServiceInMaps', false); - if (proxyElasticMapsServiceInMaps) { - const fullResponse = await fetch(`${GIS_API_RELATIVE}/${EMS_META_PATH}`); - emsSources = await fullResponse.json(); - } else { - const emsClient = new EMSClient({ - language: i18n.getLocale(), - kbnVersion: chrome.getInjected('kbnPkgVersion'), - manifestServiceUrl: chrome.getInjected('emsManifestServiceUrl'), - landingPageUrl: chrome.getInjected('emsLandingPageUrl') - }); - const isEmsEnabled = chrome.getInjected('isEmsEnabled', true); - const xpackMapsFeature = xpackInfo.get('features.maps'); - const licenseId = xpackMapsFeature && xpackMapsFeature.maps && xpackMapsFeature.uid ? xpackMapsFeature.uid : ''; - const emsResponse = await getEMSResources(emsClient, isEmsEnabled, licenseId, false); - emsSources = { - ems: { - file: emsResponse.fileLayers, - tms: emsResponse.tmsServices - } - }; - } - resolve(emsSources); - } catch (e) { - reject(e); - } - }); - return loadingMetaPromise; +function fetchFunction(...args) { + return fetch(...args); } -export async function getEmsVectorFilesMeta() { - const dataSource = await getEMSDataSources(); - return _.get(dataSource, 'ems.file', []); -} +let emsClient = null; +let latestLicenseId = null; +export function getEMSClient() { + if (!emsClient) { + const isEmsEnabled = chrome.getInjected('isEmsEnabled', true); + if (isEmsEnabled) { -export async function getEmsTMSServices() { - const dataSource = await getEMSDataSources(); - const tmsServices = _.get(dataSource, 'ems.tms', []); - return [...tmsServices]; -} + const proxyElasticMapsServiceInMaps = chrome.getInjected('proxyElasticMapsServiceInMaps', false); + const proxyPath = proxyElasticMapsServiceInMaps ? relativeToAbsolute('..') : ''; + // eslint-disable-next-line max-len + const manifestServiceUrl = proxyElasticMapsServiceInMaps ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_CATALOGUE_PATH}`) : chrome.getInjected('emsManifestServiceUrl'); -export function getKibanaRegionList() { - return chrome.getInjected('regionmapLayers'); -} + emsClient = new EMSClient({ + language: i18n.getLocale(), + kbnVersion: chrome.getInjected('kbnPkgVersion'), + manifestServiceUrl: manifestServiceUrl, + landingPageUrl: chrome.getInjected('emsLandingPageUrl'), + fetchFunction: fetchFunction, //import this from client-side, so the right instance is returned (bootstrapped from common/* would not work + proxyPath: proxyPath + }); + } else { + //EMS is turned off. Mock API. + emsClient = { + async getFileLayers() { + return []; + }, + async getTMSServices() { + return []; + }, + addQueryParams() {} + }; + } + } + const xpackMapsFeature = xpackInfo.get('features.maps'); + const licenseId = xpackMapsFeature && xpackMapsFeature.maps && xpackMapsFeature.uid ? xpackMapsFeature.uid : ''; + if (latestLicenseId !== licenseId) { + latestLicenseId = licenseId; + emsClient.addQueryParams({ license: licenseId }); + } + return emsClient; -export function getKibanaTileMap() { - return chrome.getInjected('tilemap'); } diff --git a/x-pack/legacy/plugins/maps/public/meta.test.js b/x-pack/legacy/plugins/maps/public/meta.test.js index 2a73a69d89ad..fa4813f29c74 100644 --- a/x-pack/legacy/plugins/maps/public/meta.test.js +++ b/x-pack/legacy/plugins/maps/public/meta.test.js @@ -5,7 +5,7 @@ */ import { - getEMSDataSources + getEMSClient } from './meta'; @@ -55,10 +55,18 @@ describe('default use without proxy', () => { it('should return absolute urls', async () => { + const emsClient = getEMSClient(); + const tmsServices = await emsClient.getTMSServices(); - const resources = await getEMSDataSources(); - expect(resources.ems.tms[0].url.startsWith('https://raster-style.foobar')).toBe(true); - expect(resources.ems.file[0].url.startsWith('https://vector-staging.maps.elastic.co/files')).toBe(true); - expect(resources.ems.file[1].url.startsWith('https://vector-staging.maps.elastic.co/files')).toBe(true); + const rasterUrl = await tmsServices[0].getUrlTemplate(); + expect(rasterUrl.startsWith('https://raster-style.foobar')).toBe(true); + + const fileLayers = await emsClient.getFileLayers(); + const file1Url = fileLayers[0].getDefaultFormatUrl(); + + expect(file1Url.startsWith('https://vector-staging.maps.elastic.co/files')).toBe(true); + + const file2Url = fileLayers[1].getDefaultFormatUrl(); + expect(file2Url.startsWith('https://vector-staging.maps.elastic.co/files')).toBe(true); }); }); diff --git a/x-pack/legacy/plugins/maps/public/reducers/map.js b/x-pack/legacy/plugins/maps/public/reducers/map.js index 3f30e5cbe27d..fca277515aff 100644 --- a/x-pack/legacy/plugins/maps/public/reducers/map.js +++ b/x-pack/legacy/plugins/maps/public/reducers/map.js @@ -114,7 +114,6 @@ const INITIAL_STATE = { }; - export function map(state = INITIAL_STATE, action) { switch (action.type) { case UPDATE_DRAW_STATE: diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js index 9c76be739fbe..58996a1afe41 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import { TASK_ID, scheduleTask, registerMapsTelemetryTask } from './telemetry_task'; export function initTelemetryCollection(server) { - registerMapsTelemetryTask(server.taskManager); + registerMapsTelemetryTask(server); scheduleTask(server, server.taskManager); registerMapsUsageCollector(server); } @@ -26,7 +26,7 @@ async function fetch(server) { bool: { filter: { term: { - _id: TASK_ID + _id: `task:${TASK_ID}` } } } diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js index 7fbbe8ef77ff..e3f54335eeee 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js @@ -13,37 +13,45 @@ export const TASK_ID = `Maps-${TELEMETRY_TASK_TYPE}`; export function scheduleTask(server, taskManager) { const { kbnServer } = server.plugins.xpack_main.status.plugin; - kbnServer.afterPluginsInit(async () => { - try { - await taskManager.schedule({ - id: TASK_ID, - taskType: TELEMETRY_TASK_TYPE, - state: { stats: {}, runs: 0 }, - }); - }catch(e) { - server.log(['warning', 'maps'], `Error scheduling telemetry task, received ${e.message}`); - } + kbnServer.afterPluginsInit(() => { + // The code block below can't await directly within "afterPluginsInit" + // callback due to circular dependency. The server isn't "ready" until + // this code block finishes. Migrations wait for server to be ready before + // executing. Saved objects repository waits for migrations to finish before + // finishing the request. To avoid this, we'll await within a separate + // function block. + (async () => { + try { + await taskManager.schedule({ + id: TASK_ID, + taskType: TELEMETRY_TASK_TYPE, + state: { stats: {}, runs: 0 }, + }); + }catch(e) { + server.log(['warning', 'maps'], `Error scheduling telemetry task, received ${e.message}`); + } + })(); }); } -export function registerMapsTelemetryTask(taskManager) { +export function registerMapsTelemetryTask(server) { + const taskManager = server.taskManager; taskManager.registerTaskDefinitions({ [TELEMETRY_TASK_TYPE]: { title: 'Maps telemetry fetch task', type: TELEMETRY_TASK_TYPE, timeout: '1m', numWorkers: 2, - createTaskRunner: telemetryTaskRunner(), + createTaskRunner: telemetryTaskRunner(server), }, }); } -export function telemetryTaskRunner() { +export function telemetryTaskRunner(server) { - return ({ kbnServer, taskInstance }) => { + return ({ taskInstance }) => { const { state } = taskInstance; const prevState = state; - const { server } = kbnServer; let mapsTelemetry = {}; const callCluster = server.plugins.elasticsearch.getCluster('admin') @@ -73,5 +81,5 @@ export function getNextMidnight() { const nextMidnight = new Date(); nextMidnight.setHours(0, 0, 0, 0); nextMidnight.setDate(nextMidnight.getDate() + 1); - return nextMidnight.toISOString(); + return nextMidnight; } diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.test.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.test.js index 58d53d35260c..71e92b9af5d7 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.test.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.test.js @@ -20,16 +20,15 @@ describe('telemetryTaskRunner', () => { }); test('Returns empty stats on error', async () => { - const kbnServer = { server: mockKbnServer }; const getNextMidnight = () => moment() .add(1, 'days') .startOf('day') - .toISOString(); + .toDate(); - const getRunner = telemetryTaskRunner(); + const getRunner = telemetryTaskRunner(mockKbnServer); const runResult = await getRunner( - { kbnServer, taskInstance: mockTaskInstance } + { taskInstance: mockTaskInstance } ).run(); expect(runResult).toMatchObject({ diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index bae7ce3f1756..c513a7bd17d3 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -6,16 +6,16 @@ import { - EMS_DATA_FILE_PATH, - EMS_DATA_TMS_PATH, - EMS_META_PATH, - GIS_API_PATH, - SPRITE_PATH, + EMS_CATALOGUE_PATH, + EMS_FILES_CATALOGUE_PATH, + EMS_FILES_DEFAULT_JSON_PATH, + EMS_TILES_CATALOGUE_PATH, + EMS_TILES_RASTER_STYLE_PATH, + EMS_TILES_RASTER_TILE_PATH, + GIS_API_PATH } from '../common/constants'; import fetch from 'node-fetch'; import { i18n } from '@kbn/i18n'; -import { getEMSResources } from '../common/ems_util'; -import path from 'path'; import Boom from 'boom'; @@ -26,36 +26,57 @@ export function initRoutes(server, licenseUid) { const serverConfig = server.config(); const mapConfig = serverConfig.get('map'); - const emsClient = new server.plugins.tile_map.ems_client.EMSClient({ - language: i18n.getLocale(), - kbnVersion: serverConfig.get('pkg.version'), - manifestServiceUrl: mapConfig.manifestServiceUrl, - landingPageUrl: mapConfig.emsLandingPageUrl - }); + let emsClient; + if (mapConfig.includeElasticMapsService) { + emsClient = new server.plugins.tile_map.EMSClient({ + language: i18n.getLocale(), + kbnVersion: serverConfig.get('pkg.version'), + manifestServiceUrl: mapConfig.manifestServiceUrl, + landingPageUrl: mapConfig.emsLandingPageUrl, + proxyElasticMapsServiceInMaps: false + }); + emsClient.addQueryParams({ license: licenseUid }); + } else { + emsClient = { + async getFileLayers() { + return []; + }, + async getTMSServices() { + return []; + }, + async getMainManifest() { + return null; + }, + async getDefaultFileManifest() { + return null; + }, + async getDefaultTMSManifest() { + return null; + }, + addQueryParams() {} + }; + } server.route({ method: 'GET', - path: `${ROOT}/${EMS_DATA_FILE_PATH}`, + path: `${ROOT}/${EMS_FILES_DEFAULT_JSON_PATH}`, handler: async (request) => { - if (!mapConfig.proxyElasticMapsServiceInMaps) { - server.log('warning', `Cannot load content from EMS when map.proxyElasticMapsServiceInMaps is turned off`); - throw Boom.notFound(); - } + checkEMSProxyConfig(); if (!request.query.id) { server.log('warning', 'Must supply id parameters to retrieve EMS file'); return null; } - const ems = await getEMSResources(emsClient, mapConfig.includeElasticMapsService, licenseUid, false); - const layer = ems.fileLayers.find(layer => layer.id === request.query.id); + const fileLayers = await emsClient.getFileLayers(); + const layer = fileLayers.find(layer => layer.getId() === request.query.id); if (!layer) { return null; } try { - const file = await fetch(layer.url); + const file = await fetch(layer.getDefaultFormatUrl()); return await file.json(); } catch(e) { server.log('warning', `Cannot connect to EMS for file, error: ${e.message}`); @@ -65,16 +86,12 @@ export function initRoutes(server, licenseUid) { } }); - server.route({ method: 'GET', - path: `${ROOT}/${EMS_DATA_TMS_PATH}`, + path: `${ROOT}/${EMS_TILES_RASTER_TILE_PATH}`, handler: async (request, h) => { - if (!mapConfig.proxyElasticMapsServiceInMaps) { - server.log('warning', `Cannot load content from EMS when map.proxyElasticMapsServiceInMaps is turned off`); - throw Boom.notFound(); - } + checkEMSProxyConfig(); if (!request.query.id || typeof parseInt(request.query.x, 10) !== 'number' || @@ -85,13 +102,13 @@ export function initRoutes(server, licenseUid) { return null; } - const ems = await getEMSResources(emsClient, mapConfig.includeElasticMapsService, licenseUid, false); - const tmsService = ems.tmsServices.find(layer => layer.id === request.query.id); + const tmsServices = await emsClient.getTMSServices(); + const tmsService = tmsServices.find(layer => layer.getId() === request.query.id); if (!tmsService) { - return null; - } + return null;} - const url = tmsService.url + const urlTemplate = await tmsService.getUrlTemplate(); + const url = urlTemplate .replace('{x}', request.query.x) .replace('{y}', request.query.y) .replace('{z}', request.query.z); @@ -115,34 +132,123 @@ export function initRoutes(server, licenseUid) { server.route({ method: 'GET', - path: `${ROOT}/${EMS_META_PATH}`, + path: `${ROOT}/${EMS_CATALOGUE_PATH}`, handler: async () => { - if (!mapConfig.proxyElasticMapsServiceInMaps) { - server.log('warning', `Cannot load content from EMS when map.proxyElasticMapsServiceInMaps is turned off`); - throw Boom.notFound(); - } + checkEMSProxyConfig(); - let ems; - try { - ems = await getEMSResources(emsClient, mapConfig.includeElasticMapsService, licenseUid, true); - } catch (e) { - server.log('warning', `Cannot connect to EMS, error: ${e.message}`); - ems = { - fileLayers: [], - tmsServices: [] - }; + const main = await emsClient.getMainManifest(); + const proxiedManifest = { + services: [] + }; + + //rewrite the urls to the submanifest + const tileService = main.services.find(service => service.id === 'tiles'); + const fileService = main.services.find(service => service.id === 'geo_layers'); + if (tileService) { + proxiedManifest.services.push({ + ...tileService, + manifest: `${GIS_API_PATH}/${EMS_TILES_CATALOGUE_PATH}` + }); + } + if (fileService) { + proxiedManifest.services.push({ + ...fileService, + manifest: `${GIS_API_PATH}/${EMS_FILES_CATALOGUE_PATH}` + }); } + return proxiedManifest; + } + }); + + server.route({ + method: 'GET', + path: `${ROOT}/${EMS_FILES_CATALOGUE_PATH}`, + handler: async () => { + + checkEMSProxyConfig(); + + const file = await emsClient.getDefaultFileManifest(); + const layers = file.layers.map(layer => { + const newLayer = { ...layer }; + const id = encodeURIComponent(layer.layer_id); + const newUrl = `${GIS_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}?id=${id}`; + newLayer.formats = [{ + ...layer.formats[0], + url: newUrl + }]; + return newLayer; + }); + //rewrite + return { layers }; + } + }); + + server.route({ + method: 'GET', + path: `${ROOT}/${EMS_TILES_CATALOGUE_PATH}`, + handler: async () => { - return ({ - ems: { - file: ems.fileLayers, - tms: ems.tmsServices + checkEMSProxyConfig(); + + const tilesManifest = await emsClient.getDefaultTMSManifest(); + const newServices = tilesManifest.services.map((service) => { + const newService = { + ...service + }; + const rasterFormats = service.formats.filter(format => format.format === 'raster'); + newService.formats = []; + if (rasterFormats.length) { + const newUrl = `${GIS_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}?id=${service.id}`; + newService.formats.push({ + ...rasterFormats[0], + url: newUrl + }); } + return newService; }); + + return { + services: newServices + }; } }); + server.route({ + method: 'GET', + path: `${ROOT}/${EMS_TILES_RASTER_STYLE_PATH}`, + handler: async (request) => { + + checkEMSProxyConfig(); + + if (!request.query.id) { + server.log('warning', 'Must supply id parameter to retrieve EMS raster style'); + return null; + } + + const tmsServices = await emsClient.getTMSServices(); + const tmsService = tmsServices.find(layer => layer.getId() === request.query.id); + if (!tmsService) { + return null; + } + const style = await tmsService.getDefaultRasterStyle(); + + const newUrl = `${GIS_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}?id=${request.query.id}&x={x}&y={y}&z={z}`; + return { + ...style, + tiles: [newUrl] + }; + } + }); + + + function checkEMSProxyConfig() { + if (!mapConfig.proxyElasticMapsServiceInMaps) { + server.log('warning', `Cannot load content from EMS when map.proxyElasticMapsServiceInMaps is turned off`); + throw Boom.notFound(); + } + } + server.route({ method: 'GET', path: `${ROOT}/indexCount`, @@ -154,7 +260,6 @@ export function initRoutes(server, licenseUid) { } const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - try { const { count } = await callWithRequest(request, 'count', { index: query.index }); return { count }; @@ -163,14 +268,4 @@ export function initRoutes(server, licenseUid) { } } }); - - server.route({ - method: 'GET', - path: `${SPRITE_PATH}/{path*}`, - handler: { - directory: { - path: path.join(__dirname, './sprites') - } - } - }); } diff --git a/x-pack/legacy/plugins/maps/server/sprites/maki.json b/x-pack/legacy/plugins/maps/server/sprites/maki.json deleted file mode 100644 index 8eed0d7333a2..000000000000 --- a/x-pack/legacy/plugins/maps/server/sprites/maki.json +++ /dev/null @@ -1,2820 +0,0 @@ -{ - "aerialway-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 165 - }, - "aerialway-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 0 - }, - "airfield-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 165 - }, - "airfield-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 0 - }, - "airport-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 165 - }, - "airport-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 15 - }, - "alcohol-shop-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 165 - }, - "alcohol-shop-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 15 - }, - "american-football-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 165 - }, - "american-football-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 0 - }, - "amusement-park-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 165 - }, - "amusement-park-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 0 - }, - "aquarium-11": { - "sdf": true, - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 165 - }, - "aquarium-15": { - "sdf": true, - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 15 - }, - "art-gallery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 165 - }, - "art-gallery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 15 - }, - "attraction-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 165 - }, - "attraction-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 30 - }, - "bakery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 165 - }, - "bakery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 30 - }, - "bank-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 165 - }, - "bank-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 30 - }, - "bar-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 165 - }, - "bar-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 30 - }, - "barrier-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 165 - }, - "barrier-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 45 - }, - "baseball-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 165 - }, - "baseball-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 45 - }, - "basketball-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 165 - }, - "basketball-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 45 - }, - "bbq-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 165 - }, - "bbq-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 45 - }, - "beach-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 165 - }, - "beach-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 0 - }, - "beer-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 165 - }, - "beer-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 0 - }, - "bicycle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 165 - }, - "bicycle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 0 - }, - "bicycle-share-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 165 - }, - "bicycle-share-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 0 - }, - "blood-bank-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 165 - }, - "blood-bank-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 15 - }, - "bowling-alley-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 176 - }, - "bowling-alley-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 15 - }, - "bridge-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 176 - }, - "bridge-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 15 - }, - "building-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 176 - }, - "building-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 15 - }, - "building-alt1-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 176 - }, - "building-alt1-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 30 - }, - "bus-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 176 - }, - "bus-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 30 - }, - "cafe-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 176 - }, - "cafe-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 30 - }, - "campsite-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 176 - }, - "campsite-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 30 - }, - "car-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 176 - }, - "car-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 45 - }, - "car-rental-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 176 - }, - "car-rental-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 45 - }, - "car-repair-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 176 - }, - "car-repair-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 45 - }, - "casino-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 176 - }, - "casino-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 45 - }, - "castle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 176 - }, - "castle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 60 - }, - "cemetery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 176 - }, - "cemetery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 60 - }, - "charging-station-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 176 - }, - "charging-station-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 60 - }, - "cinema-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 176 - }, - "cinema-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 60 - }, - "circle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 176 - }, - "circle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 60 - }, - "circle-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 176 - }, - "circle-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 60 - }, - "city-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 176 - }, - "city-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 60 - }, - "clothing-store-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 176 - }, - "clothing-store-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 60 - }, - "college-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 176 - }, - "college-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 75 - }, - "commercial-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 176 - }, - "commercial-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 75 - }, - "communications-tower-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 187 - }, - "communications-tower-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 75 - }, - "confectionery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 187 - }, - "confectionery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 75 - }, - "convenience-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 187 - }, - "convenience-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 75 - }, - "cricket-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 187 - }, - "cricket-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 75 - }, - "cross-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 187 - }, - "cross-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 75 - }, - "dam-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 187 - }, - "dam-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 75 - }, - "danger-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 187 - }, - "danger-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 90 - }, - "defibrillator-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 187 - }, - "defibrillator-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 90 - }, - "dentist-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 187 - }, - "dentist-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 90 - }, - "doctor-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 187 - }, - "doctor-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 90 - }, - "dog-park-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 187 - }, - "dog-park-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 90 - }, - "drinking-water-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 187 - }, - "drinking-water-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 90 - }, - "embassy-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 187 - }, - "embassy-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 90 - }, - "emergency-phone-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 187 - }, - "emergency-phone-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 90 - }, - "entrance-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 187 - }, - "entrance-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 105 - }, - "entrance-alt1-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 187 - }, - "entrance-alt1-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 105 - }, - "farm-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 187 - }, - "farm-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 105 - }, - "fast-food-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 187 - }, - "fast-food-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 105 - }, - "fence-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 187 - }, - "fence-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 105 - }, - "ferry-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 187 - }, - "ferry-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 105 - }, - "fire-station-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 187 - }, - "fire-station-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 105 - }, - "fitness-centre-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 198 - }, - "fitness-centre-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 105 - }, - "florist-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 198 - }, - "florist-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 0 - }, - "fuel-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 198 - }, - "fuel-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 0 - }, - "furniture-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 198 - }, - "furniture-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 0 - }, - "gaming-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 198 - }, - "gaming-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 0 - }, - "garden-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 198 - }, - "garden-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 0 - }, - "garden-centre-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 198 - }, - "garden-centre-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 0 - }, - "gift-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 198 - }, - "gift-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 0 - }, - "globe-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 198 - }, - "globe-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 0 - }, - "golf-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 198 - }, - "golf-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 15 - }, - "grocery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 198 - }, - "grocery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 15 - }, - "hairdresser-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 198 - }, - "hairdresser-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 15 - }, - "harbor-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 198 - }, - "harbor-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 15 - }, - "hardware-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 198 - }, - "hardware-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 15 - }, - "heart-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 198 - }, - "heart-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 15 - }, - "heliport-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 198 - }, - "heliport-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 15 - }, - "home-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 198 - }, - "home-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 15 - }, - "horse-riding-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 198 - }, - "horse-riding-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 30 - }, - "hospital-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 198 - }, - "hospital-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 30 - }, - "ice-cream-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 198 - }, - "ice-cream-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 30 - }, - "industry-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 198 - }, - "industry-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 30 - }, - "information-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 209 - }, - "information-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 30 - }, - "jewelry-store-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 209 - }, - "jewelry-store-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 30 - }, - "karaoke-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 209 - }, - "karaoke-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 30 - }, - "landmark-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 209 - }, - "landmark-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 30 - }, - "landuse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 209 - }, - "landuse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 45 - }, - "laundry-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 209 - }, - "laundry-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 45 - }, - "library-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 209 - }, - "library-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 45 - }, - "lighthouse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 209 - }, - "lighthouse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 45 - }, - "lodging-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 209 - }, - "lodging-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 45 - }, - "logging-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 209 - }, - "logging-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 45 - }, - "marker-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 209 - }, - "marker-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 45 - }, - "marker-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 209 - }, - "marker-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 45 - }, - "mobile-phone-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 209 - }, - "mobile-phone-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 60 - }, - "monument-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 209 - }, - "monument-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 60 - }, - "mountain-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 209 - }, - "mountain-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 60 - }, - "museum-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 209 - }, - "museum-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 60 - }, - "music-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 209 - }, - "music-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 60 - }, - "natural-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 209 - }, - "natural-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 60 - }, - "optician-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 209 - }, - "optician-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 60 - }, - "paint-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 209 - }, - "paint-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 60 - }, - "park-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 209 - }, - "park-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 75 - }, - "park-alt1-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 220 - }, - "park-alt1-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 75 - }, - "parking-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 220 - }, - "parking-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 75 - }, - "parking-garage-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 220 - }, - "parking-garage-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 75 - }, - "pharmacy-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 220 - }, - "pharmacy-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 75 - }, - "picnic-site-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 220 - }, - "picnic-site-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 75 - }, - "pitch-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 220 - }, - "pitch-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 75 - }, - "place-of-worship-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 220 - }, - "place-of-worship-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 75 - }, - "playground-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 220 - }, - "playground-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 90 - }, - "police-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 220 - }, - "police-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 90 - }, - "post-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 220 - }, - "post-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 90 - }, - "prison-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 220 - }, - "prison-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 90 - }, - "rail-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 220 - }, - "rail-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 90 - }, - "rail-light-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 220 - }, - "rail-light-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 90 - }, - "rail-metro-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 220 - }, - "rail-metro-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 90 - }, - "ranger-station-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 220 - }, - "ranger-station-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 90 - }, - "recycling-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 220 - }, - "recycling-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 105 - }, - "religious-buddhist-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 220 - }, - "religious-buddhist-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 105 - }, - "religious-christian-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 220 - }, - "religious-christian-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 105 - }, - "religious-jewish-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 220 - }, - "religious-jewish-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 105 - }, - "religious-muslim-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 220 - }, - "religious-muslim-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 105 - }, - "residential-community-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 220 - }, - "residential-community-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 105 - }, - "restaurant-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 231, - "y": 165 - }, - "restaurant-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 105 - }, - "restaurant-noodle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 242, - "y": 165 - }, - "restaurant-noodle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 105 - }, - "restaurant-pizza-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 253, - "y": 165 - }, - "restaurant-pizza-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 120 - }, - "restaurant-seafood-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 264, - "y": 165 - }, - "restaurant-seafood-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 120 - }, - "roadblock-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 275, - "y": 165 - }, - "roadblock-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 120 - }, - "rocket-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 286, - "y": 165 - }, - "rocket-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 120 - }, - "school-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 297, - "y": 165 - }, - "school-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 120 - }, - "scooter-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 308, - "y": 165 - }, - "scooter-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 120 - }, - "shelter-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 319, - "y": 165 - }, - "shelter-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 120 - }, - "shoe-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 330, - "y": 165 - }, - "shoe-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 120 - }, - "shop-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 341, - "y": 165 - }, - "shop-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 120 - }, - "skateboard-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 352, - "y": 165 - }, - "skateboard-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 120 - }, - "skiing-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 363, - "y": 165 - }, - "skiing-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 120 - }, - "slaughterhouse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 374, - "y": 165 - }, - "slaughterhouse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 120 - }, - "slipway-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 385, - "y": 165 - }, - "slipway-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 120 - }, - "snowmobile-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 396, - "y": 165 - }, - "snowmobile-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 120 - }, - "soccer-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 407, - "y": 165 - }, - "soccer-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 120 - }, - "square-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 418, - "y": 165 - }, - "square-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 120 - }, - "square-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 429, - "y": 165 - }, - "square-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 135 - }, - "stadium-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 440, - "y": 165 - }, - "stadium-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 135 - }, - "star-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 451, - "y": 165 - }, - "star-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 135 - }, - "star-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 462, - "y": 165 - }, - "star-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 135 - }, - "suitcase-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 231, - "y": 176 - }, - "suitcase-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 135 - }, - "sushi-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 242, - "y": 176 - }, - "sushi-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 135 - }, - "swimming-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 253, - "y": 176 - }, - "swimming-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 135 - }, - "table-tennis-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 264, - "y": 176 - }, - "table-tennis-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 135 - }, - "teahouse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 275, - "y": 176 - }, - "teahouse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 135 - }, - "telephone-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 286, - "y": 176 - }, - "telephone-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 135 - }, - "tennis-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 297, - "y": 176 - }, - "tennis-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 135 - }, - "theatre-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 308, - "y": 176 - }, - "theatre-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 135 - }, - "toilet-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 319, - "y": 176 - }, - "toilet-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 135 - }, - "town-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 330, - "y": 176 - }, - "town-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 135 - }, - "town-hall-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 341, - "y": 176 - }, - "town-hall-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 135 - }, - "triangle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 352, - "y": 176 - }, - "triangle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 135 - }, - "triangle-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 363, - "y": 176 - }, - "triangle-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 150 - }, - "veterinary-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 374, - "y": 176 - }, - "veterinary-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 150 - }, - "viewpoint-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 385, - "y": 176 - }, - "viewpoint-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 150 - }, - "village-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 396, - "y": 176 - }, - "village-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 150 - }, - "volcano-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 407, - "y": 176 - }, - "volcano-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 150 - }, - "volleyball-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 418, - "y": 176 - }, - "volleyball-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 150 - }, - "warehouse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 429, - "y": 176 - }, - "warehouse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 150 - }, - "waste-basket-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 440, - "y": 176 - }, - "waste-basket-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 150 - }, - "watch-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 451, - "y": 176 - }, - "watch-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 150 - }, - "water-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 462, - "y": 176 - }, - "water-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 150 - }, - "waterfall-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 231, - "y": 187 - }, - "waterfall-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 150 - }, - "watermill-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 242, - "y": 187 - }, - "watermill-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 150 - }, - "wetland-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 253, - "y": 187 - }, - "wetland-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 150 - }, - "wheelchair-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 264, - "y": 187 - }, - "wheelchair-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 150 - }, - "windmill-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 275, - "y": 187 - }, - "windmill-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 150 - }, - "zoo-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 286, - "y": 187 - }, - "zoo-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 150 - } -} diff --git a/x-pack/legacy/plugins/maps/server/sprites/maki.png b/x-pack/legacy/plugins/maps/server/sprites/maki.png deleted file mode 100644 index 9206cf8c0def..000000000000 Binary files a/x-pack/legacy/plugins/maps/server/sprites/maki.png and /dev/null differ diff --git a/x-pack/legacy/plugins/maps/server/sprites/maki@2x.json b/x-pack/legacy/plugins/maps/server/sprites/maki@2x.json deleted file mode 100644 index 2dff0d829281..000000000000 --- a/x-pack/legacy/plugins/maps/server/sprites/maki@2x.json +++ /dev/null @@ -1,2820 +0,0 @@ -{ - "aerialway-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 0, - "y": 330 - }, - "aerialway-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 0 - }, - "airfield-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 22, - "y": 330 - }, - "airfield-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 0 - }, - "airport-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 44, - "y": 330 - }, - "airport-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 30 - }, - "alcohol-shop-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 66, - "y": 330 - }, - "alcohol-shop-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 30 - }, - "american-football-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 88, - "y": 330 - }, - "american-football-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 0 - }, - "amusement-park-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 110, - "y": 330 - }, - "amusement-park-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 0 - }, - "aquarium-11": { - "sdf": true, - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 132, - "y": 330 - }, - "aquarium-15": { - "sdf": true, - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 30 - }, - "art-gallery-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 154, - "y": 330 - }, - "art-gallery-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 30 - }, - "attraction-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 176, - "y": 330 - }, - "attraction-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 60 - }, - "bakery-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 198, - "y": 330 - }, - "bakery-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 60 - }, - "bank-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 220, - "y": 330 - }, - "bank-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 60 - }, - "bar-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 242, - "y": 330 - }, - "bar-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 60 - }, - "barrier-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 264, - "y": 330 - }, - "barrier-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 90 - }, - "baseball-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 286, - "y": 330 - }, - "baseball-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 90 - }, - "basketball-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 308, - "y": 330 - }, - "basketball-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 90 - }, - "bbq-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 330, - "y": 330 - }, - "bbq-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 90 - }, - "beach-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 352, - "y": 330 - }, - "beach-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 0 - }, - "beer-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 374, - "y": 330 - }, - "beer-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 0 - }, - "bicycle-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 396, - "y": 330 - }, - "bicycle-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 0 - }, - "bicycle-share-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 418, - "y": 330 - }, - "bicycle-share-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 0 - }, - "blood-bank-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 440, - "y": 330 - }, - "blood-bank-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 30 - }, - "bowling-alley-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 0, - "y": 352 - }, - "bowling-alley-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 30 - }, - "bridge-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 22, - "y": 352 - }, - "bridge-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 30 - }, - "building-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 44, - "y": 352 - }, - "building-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 30 - }, - "building-alt1-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 66, - "y": 352 - }, - "building-alt1-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 60 - }, - "bus-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 88, - "y": 352 - }, - "bus-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 60 - }, - "cafe-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 110, - "y": 352 - }, - "cafe-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 60 - }, - "campsite-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 132, - "y": 352 - }, - "campsite-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 60 - }, - "car-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 154, - "y": 352 - }, - "car-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 90 - }, - "car-rental-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 176, - "y": 352 - }, - "car-rental-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 90 - }, - "car-repair-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 198, - "y": 352 - }, - "car-repair-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 90 - }, - "casino-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 220, - "y": 352 - }, - "casino-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 90 - }, - "castle-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 242, - "y": 352 - }, - "castle-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 120 - }, - "cemetery-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 264, - "y": 352 - }, - "cemetery-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 120 - }, - "charging-station-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 286, - "y": 352 - }, - "charging-station-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 120 - }, - "cinema-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 308, - "y": 352 - }, - "cinema-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 120 - }, - "circle-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 330, - "y": 352 - }, - "circle-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 120 - }, - "circle-stroked-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 352, - "y": 352 - }, - "circle-stroked-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 120 - }, - "city-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 374, - "y": 352 - }, - "city-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 120 - }, - "clothing-store-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 396, - "y": 352 - }, - "clothing-store-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 120 - }, - "college-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 418, - "y": 352 - }, - "college-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 150 - }, - "commercial-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 440, - "y": 352 - }, - "commercial-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 150 - }, - "communications-tower-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 0, - "y": 374 - }, - "communications-tower-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 150 - }, - "confectionery-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 22, - "y": 374 - }, - "confectionery-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 150 - }, - "convenience-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 44, - "y": 374 - }, - "convenience-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 150 - }, - "cricket-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 66, - "y": 374 - }, - "cricket-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 150 - }, - "cross-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 88, - "y": 374 - }, - "cross-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 150 - }, - "dam-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 110, - "y": 374 - }, - "dam-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 150 - }, - "danger-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 132, - "y": 374 - }, - "danger-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 180 - }, - "defibrillator-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 154, - "y": 374 - }, - "defibrillator-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 180 - }, - "dentist-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 176, - "y": 374 - }, - "dentist-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 180 - }, - "doctor-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 198, - "y": 374 - }, - "doctor-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 180 - }, - "dog-park-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 220, - "y": 374 - }, - "dog-park-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 180 - }, - "drinking-water-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 242, - "y": 374 - }, - "drinking-water-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 180 - }, - "embassy-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 264, - "y": 374 - }, - "embassy-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 180 - }, - "emergency-phone-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 286, - "y": 374 - }, - "emergency-phone-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 180 - }, - "entrance-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 308, - "y": 374 - }, - "entrance-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 210 - }, - "entrance-alt1-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 330, - "y": 374 - }, - "entrance-alt1-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 210 - }, - "farm-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 352, - "y": 374 - }, - "farm-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 210 - }, - "fast-food-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 374, - "y": 374 - }, - "fast-food-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 210 - }, - "fence-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 396, - "y": 374 - }, - "fence-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 210 - }, - "ferry-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 418, - "y": 374 - }, - "ferry-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 210 - }, - "fire-station-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 440, - "y": 374 - }, - "fire-station-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 210 - }, - "fitness-centre-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 0, - "y": 396 - }, - "fitness-centre-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 210 - }, - "florist-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 22, - "y": 396 - }, - "florist-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 0 - }, - "fuel-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 44, - "y": 396 - }, - "fuel-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 0 - }, - "furniture-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 66, - "y": 396 - }, - "furniture-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 0 - }, - "gaming-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 88, - "y": 396 - }, - "gaming-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 0 - }, - "garden-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 110, - "y": 396 - }, - "garden-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 0 - }, - "garden-centre-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 132, - "y": 396 - }, - "garden-centre-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 0 - }, - "gift-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 154, - "y": 396 - }, - "gift-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 0 - }, - "globe-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 176, - "y": 396 - }, - "globe-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 0 - }, - "golf-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 198, - "y": 396 - }, - "golf-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 30 - }, - "grocery-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 220, - "y": 396 - }, - "grocery-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 30 - }, - "hairdresser-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 242, - "y": 396 - }, - "hairdresser-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 30 - }, - "harbor-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 264, - "y": 396 - }, - "harbor-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 30 - }, - "hardware-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 286, - "y": 396 - }, - "hardware-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 30 - }, - "heart-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 308, - "y": 396 - }, - "heart-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 30 - }, - "heliport-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 330, - "y": 396 - }, - "heliport-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 30 - }, - "home-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 352, - "y": 396 - }, - "home-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 30 - }, - "horse-riding-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 374, - "y": 396 - }, - "horse-riding-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 60 - }, - "hospital-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 396, - "y": 396 - }, - "hospital-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 60 - }, - "ice-cream-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 418, - "y": 396 - }, - "ice-cream-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 60 - }, - "industry-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 440, - "y": 396 - }, - "industry-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 60 - }, - "information-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 0, - "y": 418 - }, - "information-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 60 - }, - "jewelry-store-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 22, - "y": 418 - }, - "jewelry-store-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 60 - }, - "karaoke-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 44, - "y": 418 - }, - "karaoke-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 60 - }, - "landmark-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 66, - "y": 418 - }, - "landmark-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 60 - }, - "landuse-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 88, - "y": 418 - }, - "landuse-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 90 - }, - "laundry-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 110, - "y": 418 - }, - "laundry-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 90 - }, - "library-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 132, - "y": 418 - }, - "library-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 90 - }, - "lighthouse-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 154, - "y": 418 - }, - "lighthouse-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 90 - }, - "lodging-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 176, - "y": 418 - }, - "lodging-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 90 - }, - "logging-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 198, - "y": 418 - }, - "logging-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 90 - }, - "marker-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 220, - "y": 418 - }, - "marker-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 90 - }, - "marker-stroked-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 242, - "y": 418 - }, - "marker-stroked-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 90 - }, - "mobile-phone-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 264, - "y": 418 - }, - "mobile-phone-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 120 - }, - "monument-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 286, - "y": 418 - }, - "monument-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 120 - }, - "mountain-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 308, - "y": 418 - }, - "mountain-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 120 - }, - "museum-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 330, - "y": 418 - }, - "museum-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 120 - }, - "music-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 352, - "y": 418 - }, - "music-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 120 - }, - "natural-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 374, - "y": 418 - }, - "natural-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 120 - }, - "optician-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 396, - "y": 418 - }, - "optician-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 120 - }, - "paint-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 418, - "y": 418 - }, - "paint-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 120 - }, - "park-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 440, - "y": 418 - }, - "park-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 150 - }, - "park-alt1-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 0, - "y": 440 - }, - "park-alt1-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 150 - }, - "parking-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 22, - "y": 440 - }, - "parking-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 150 - }, - "parking-garage-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 44, - "y": 440 - }, - "parking-garage-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 150 - }, - "pharmacy-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 66, - "y": 440 - }, - "pharmacy-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 150 - }, - "picnic-site-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 88, - "y": 440 - }, - "picnic-site-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 150 - }, - "pitch-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 110, - "y": 440 - }, - "pitch-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 150 - }, - "place-of-worship-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 132, - "y": 440 - }, - "place-of-worship-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 150 - }, - "playground-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 154, - "y": 440 - }, - "playground-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 180 - }, - "police-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 176, - "y": 440 - }, - "police-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 180 - }, - "post-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 198, - "y": 440 - }, - "post-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 180 - }, - "prison-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 220, - "y": 440 - }, - "prison-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 180 - }, - "rail-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 242, - "y": 440 - }, - "rail-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 180 - }, - "rail-light-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 264, - "y": 440 - }, - "rail-light-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 180 - }, - "rail-metro-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 286, - "y": 440 - }, - "rail-metro-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 180 - }, - "ranger-station-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 308, - "y": 440 - }, - "ranger-station-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 180 - }, - "recycling-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 330, - "y": 440 - }, - "recycling-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 210 - }, - "religious-buddhist-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 352, - "y": 440 - }, - "religious-buddhist-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 210 - }, - "religious-christian-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 374, - "y": 440 - }, - "religious-christian-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 210 - }, - "religious-jewish-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 396, - "y": 440 - }, - "religious-jewish-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 210 - }, - "religious-muslim-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 418, - "y": 440 - }, - "religious-muslim-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 210 - }, - "residential-community-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 440, - "y": 440 - }, - "residential-community-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 210 - }, - "restaurant-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 462, - "y": 330 - }, - "restaurant-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 210 - }, - "restaurant-noodle-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 484, - "y": 330 - }, - "restaurant-noodle-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 210 - }, - "restaurant-pizza-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 506, - "y": 330 - }, - "restaurant-pizza-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 240 - }, - "restaurant-seafood-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 528, - "y": 330 - }, - "restaurant-seafood-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 240 - }, - "roadblock-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 550, - "y": 330 - }, - "roadblock-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 240 - }, - "rocket-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 572, - "y": 330 - }, - "rocket-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 240 - }, - "school-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 594, - "y": 330 - }, - "school-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 240 - }, - "scooter-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 616, - "y": 330 - }, - "scooter-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 240 - }, - "shelter-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 638, - "y": 330 - }, - "shelter-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 240 - }, - "shoe-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 660, - "y": 330 - }, - "shoe-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 240 - }, - "shop-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 682, - "y": 330 - }, - "shop-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 240 - }, - "skateboard-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 704, - "y": 330 - }, - "skateboard-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 240 - }, - "skiing-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 726, - "y": 330 - }, - "skiing-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 240 - }, - "slaughterhouse-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 748, - "y": 330 - }, - "slaughterhouse-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 240 - }, - "slipway-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 770, - "y": 330 - }, - "slipway-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 240 - }, - "snowmobile-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 792, - "y": 330 - }, - "snowmobile-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 240 - }, - "soccer-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 814, - "y": 330 - }, - "soccer-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 240 - }, - "square-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 836, - "y": 330 - }, - "square-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 240 - }, - "square-stroked-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 858, - "y": 330 - }, - "square-stroked-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 270 - }, - "stadium-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 880, - "y": 330 - }, - "stadium-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 270 - }, - "star-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 902, - "y": 330 - }, - "star-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 270 - }, - "star-stroked-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 924, - "y": 330 - }, - "star-stroked-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 270 - }, - "suitcase-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 462, - "y": 352 - }, - "suitcase-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 270 - }, - "sushi-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 484, - "y": 352 - }, - "sushi-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 270 - }, - "swimming-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 506, - "y": 352 - }, - "swimming-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 270 - }, - "table-tennis-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 528, - "y": 352 - }, - "table-tennis-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 270 - }, - "teahouse-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 550, - "y": 352 - }, - "teahouse-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 270 - }, - "telephone-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 572, - "y": 352 - }, - "telephone-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 270 - }, - "tennis-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 594, - "y": 352 - }, - "tennis-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 270 - }, - "theatre-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 616, - "y": 352 - }, - "theatre-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 270 - }, - "toilet-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 638, - "y": 352 - }, - "toilet-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 270 - }, - "town-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 660, - "y": 352 - }, - "town-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 270 - }, - "town-hall-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 682, - "y": 352 - }, - "town-hall-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 270 - }, - "triangle-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 704, - "y": 352 - }, - "triangle-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 270 - }, - "triangle-stroked-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 726, - "y": 352 - }, - "triangle-stroked-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 0, - "y": 300 - }, - "veterinary-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 748, - "y": 352 - }, - "veterinary-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 30, - "y": 300 - }, - "viewpoint-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 770, - "y": 352 - }, - "viewpoint-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 60, - "y": 300 - }, - "village-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 792, - "y": 352 - }, - "village-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 90, - "y": 300 - }, - "volcano-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 814, - "y": 352 - }, - "volcano-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 120, - "y": 300 - }, - "volleyball-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 836, - "y": 352 - }, - "volleyball-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 150, - "y": 300 - }, - "warehouse-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 858, - "y": 352 - }, - "warehouse-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 180, - "y": 300 - }, - "waste-basket-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 880, - "y": 352 - }, - "waste-basket-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 210, - "y": 300 - }, - "watch-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 902, - "y": 352 - }, - "watch-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 240, - "y": 300 - }, - "water-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 924, - "y": 352 - }, - "water-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 270, - "y": 300 - }, - "waterfall-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 462, - "y": 374 - }, - "waterfall-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 300, - "y": 300 - }, - "watermill-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 484, - "y": 374 - }, - "watermill-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 330, - "y": 300 - }, - "wetland-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 506, - "y": 374 - }, - "wetland-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 360, - "y": 300 - }, - "wheelchair-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 528, - "y": 374 - }, - "wheelchair-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 390, - "y": 300 - }, - "windmill-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 550, - "y": 374 - }, - "windmill-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 420, - "y": 300 - }, - "zoo-11": { - "sdf": true, - "height": 22, - "pixelRatio": 2, - "width": 22, - "x": 572, - "y": 374 - }, - "zoo-15": { - "sdf": true, - "height": 30, - "pixelRatio": 2, - "width": 30, - "x": 450, - "y": 300 - } -} diff --git a/x-pack/legacy/plugins/maps/server/sprites/maki@2x.png b/x-pack/legacy/plugins/maps/server/sprites/maki@2x.png deleted file mode 100644 index e9244ba20e19..000000000000 Binary files a/x-pack/legacy/plugins/maps/server/sprites/maki@2x.png and /dev/null differ diff --git a/x-pack/legacy/plugins/ml/common/constants/aggregation_types.ts b/x-pack/legacy/plugins/ml/common/constants/aggregation_types.ts new file mode 100644 index 000000000000..09173247237a --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/constants/aggregation_types.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. + */ + +export enum ML_JOB_AGGREGATION { + COUNT = 'count', + HIGH_COUNT = 'high_count', + LOW_COUNT = 'low_count', + MEAN = 'mean', + HIGH_MEAN = 'high_mean', + LOW_MEAN = 'low_mean', + SUM = 'sum', + HIGH_SUM = 'high_sum', + LOW_SUM = 'low_sum', + MEDIAN = 'median', + HIGH_MEDIAN = 'high_median', + LOW_MEDIAN = 'low_median', + MIN = 'min', + MAX = 'max', + DISTINCT_COUNT = 'distinct_count', +} + +export enum KIBANA_AGGREGATION { + COUNT = 'count', + AVG = 'avg', + MAX = 'max', + MIN = 'min', + SUM = 'sum', + MEDIAN = 'median', + CARDINALITY = 'cardinality', +} + +export enum ES_AGGREGATION { + COUNT = 'count', + AVG = 'avg', + MAX = 'max', + MIN = 'min', + SUM = 'sum', + PERCENTILES = 'percentiles', + CARDINALITY = 'cardinality', +} diff --git a/x-pack/legacy/plugins/ml/common/constants/anomalies.ts b/x-pack/legacy/plugins/ml/common/constants/anomalies.ts new file mode 100644 index 000000000000..bbf3616c0588 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/constants/anomalies.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 enum ANOMALY_SEVERITY { + CRITICAL = 'critical', + MAJOR = 'major', + MINOR = 'minor', + WARNING = 'warning', + LOW = 'low', + UNKNOWN = 'unknown', +} + +export enum ANOMALY_THRESHOLD { + CRITICAL = 75, + MAJOR = 50, + MINOR = 25, + WARNING = 3, + LOW = 0, +} diff --git a/x-pack/legacy/plugins/ml/common/constants/search.ts b/x-pack/legacy/plugins/ml/common/constants/search.ts index 2ea27c5b5322..e17f6b309842 100644 --- a/x-pack/legacy/plugins/ml/common/constants/search.ts +++ b/x-pack/legacy/plugins/ml/common/constants/search.ts @@ -6,3 +6,8 @@ export const ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE = 500; export const ANOMALIES_TABLE_DEFAULT_QUERY_SIZE = 500; + +export enum SEARCH_QUERY_LANGUAGE { + KUERY = 'kuery', + LUCENE = 'lucene', +} diff --git a/x-pack/legacy/plugins/ml/common/constants/states.js b/x-pack/legacy/plugins/ml/common/constants/states.js deleted file mode 100644 index 4584171c713f..000000000000 --- a/x-pack/legacy/plugins/ml/common/constants/states.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - - -export const DATAFEED_STATE = { - STARTED: 'started', - STARTING: 'starting', - STOPPED: 'stopped', - STOPPING: 'stopping', - DELETED: 'deleted', -}; - -export const FORECAST_REQUEST_STATE = { - FAILED: 'failed', - FINISHED: 'finished', - SCHEDULED: 'scheduled', - STARTED: 'started', -}; - -export const JOB_STATE = { - CLOSED: 'closed', - CLOSING: 'closing', - FAILED: 'failed', - OPENED: 'opened', - OPENING: 'opening', - DELETED: 'deleted', -}; diff --git a/x-pack/legacy/plugins/ml/common/constants/states.ts b/x-pack/legacy/plugins/ml/common/constants/states.ts new file mode 100644 index 000000000000..30c3e44b7f43 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/constants/states.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. + */ + +export enum DATAFEED_STATE { + STARTED = 'started', + STARTING = 'starting', + STOPPED = 'stopped', + STOPPING = 'stopping', + DELETED = 'deleted', +} + +export enum FORECAST_REQUEST_STATE { + FAILED = 'failed', + FINISHED = 'finished', + SCHEDULED = 'scheduled', + STARTED = 'started', +} + +export enum JOB_STATE { + CLOSED = 'closed', + CLOSING = 'closing', + FAILED = 'failed', + OPENED = 'opened', + OPENING = 'opening', + DELETED = 'deleted', +} diff --git a/x-pack/legacy/plugins/ml/common/constants/validation.js b/x-pack/legacy/plugins/ml/common/constants/validation.js deleted file mode 100644 index 914a1ab3215d..000000000000 --- a/x-pack/legacy/plugins/ml/common/constants/validation.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - - -export const VALIDATION_STATUS = { - ERROR: 'error', - INFO: 'info', - SUCCESS: 'success', - WARNING: 'warning' -}; - -export const SKIP_BUCKET_SPAN_ESTIMATION = true; - -export const ALLOWED_DATA_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; diff --git a/x-pack/legacy/plugins/ml/common/constants/validation.ts b/x-pack/legacy/plugins/ml/common/constants/validation.ts new file mode 100644 index 000000000000..c71db4dca3c9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/constants/validation.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. + */ + +export enum VALIDATION_STATUS { + ERROR = 'error', + INFO = 'info', + SUCCESS = 'success', + WARNING = 'warning', +} + +export const SKIP_BUCKET_SPAN_ESTIMATION = true; + +export const ALLOWED_DATA_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; diff --git a/x-pack/legacy/plugins/ml/common/types/audit_message.ts b/x-pack/legacy/plugins/ml/common/types/audit_message.ts index 4821854333ce..c34abd669076 100644 --- a/x-pack/legacy/plugins/ml/common/types/audit_message.ts +++ b/x-pack/legacy/plugins/ml/common/types/audit_message.ts @@ -12,6 +12,10 @@ export interface AuditMessageBase { text?: string; } +export interface AnalyticsMessage extends AuditMessageBase { + analytics_id: string; +} + export interface TransformMessage extends AuditMessageBase { transform_id: string; } diff --git a/x-pack/legacy/plugins/ml/common/types/fields.ts b/x-pack/legacy/plugins/ml/common/types/fields.ts new file mode 100644 index 000000000000..7e09f14515a1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/types/fields.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 { ES_FIELD_TYPES } from '../../common/constants/field_types'; +import { ML_JOB_AGGREGATION } from '../../common/constants/aggregation_types'; + +export const EVENT_RATE_FIELD_ID = '__ml_event_rate_count__'; + +export type FieldId = string; +export type AggId = ML_JOB_AGGREGATION; +export type SplitField = Field | null; +export type DslName = string; + +export interface Field { + id: FieldId; + name: string; + type: ES_FIELD_TYPES; + aggregatable: boolean; + aggIds?: AggId[]; + aggs?: Aggregation[]; +} + +export interface Aggregation { + id: AggId; + title: string; + kibanaName: string; + dslName: DslName; + type: string; + mlModelPlotAgg: { + min: string; + max: string; + }; + fieldIds?: FieldId[]; + fields?: Field[]; +} + +export interface NewJobCaps { + fields: Field[]; + aggs: Aggregation[]; +} + +export interface AggFieldPair { + agg: Aggregation; + field: Field; + by?: { + field: SplitField; + value: string | null; + }; +} + +export interface AggFieldNamePair { + agg: string; + field: string; + by?: { + field: string | null; + value: string | null; + }; +} diff --git a/x-pack/legacy/plugins/ml/common/types/kibana.ts b/x-pack/legacy/plugins/ml/common/types/kibana.ts index 88e5f68fb4a3..86db2ce59d7e 100644 --- a/x-pack/legacy/plugins/ml/common/types/kibana.ts +++ b/x-pack/legacy/plugins/ml/common/types/kibana.ts @@ -4,4 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +// custom edits or fixes for default kibana types which are incomplete + +export type IndexPatternTitle = string; + export type callWithRequestType = (action: string, params?: any) => Promise; + +export interface Route { + id: string; + k7Breadcrumbs: () => any; +} diff --git a/x-pack/legacy/plugins/ml/common/types/privileges.ts b/x-pack/legacy/plugins/ml/common/types/privileges.ts index e229acc27de6..83890012c4e2 100644 --- a/x-pack/legacy/plugins/ml/common/types/privileges.ts +++ b/x-pack/legacy/plugins/ml/common/types/privileges.ts @@ -28,12 +28,17 @@ export interface Privileges { canDeleteFilter: boolean; // File Data Visualizer canFindFileStructure: boolean; - // Data Frames + // Data Frame Transforms canGetDataFrame: boolean; canDeleteDataFrame: boolean; canPreviewDataFrame: boolean; canCreateDataFrame: boolean; canStartStopDataFrame: boolean; + // Data Frame Analytics + canGetDataFrameAnalytics: boolean; + canDeleteDataFrameAnalytics: boolean; + canCreateDataFrameAnalytics: boolean; + canStartStopDataFrameAnalytics: boolean; } export function getDefaultPrivileges(): Privileges { @@ -60,11 +65,23 @@ export function getDefaultPrivileges(): Privileges { canDeleteFilter: false, // File Data Visualizer canFindFileStructure: false, - // Data Frames + // Data Frame Transforms canGetDataFrame: false, canDeleteDataFrame: false, canPreviewDataFrame: false, canCreateDataFrame: false, canStartStopDataFrame: false, + // Data Frame Analytics + canGetDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canCreateDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, }; } + +export interface PrivilegesResponse { + capabilities: Privileges; + upgradeInProgress: boolean; + isPlatinumOrTrialLicense: boolean; + mlFeatureEnabledInSpace: boolean; +} diff --git a/x-pack/legacy/plugins/ml/common/util/__tests__/job_utils.js b/x-pack/legacy/plugins/ml/common/util/__tests__/job_utils.js index ae450ab82f7c..737dd6254316 100644 --- a/x-pack/legacy/plugins/ml/common/util/__tests__/job_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/__tests__/job_utils.js @@ -521,12 +521,12 @@ describe('ML - job utils', () => { describe('prefixDatafeedId', () => { - it('returns datafeed-prefix-job"', () => { + it('returns datafeed-prefix-job from datafeed-job"', () => { expect(prefixDatafeedId('datafeed-job', 'prefix-')).to.be('datafeed-prefix-job'); }); - it('returns prefix-job"', () => { - expect(prefixDatafeedId('job', 'prefix-')).to.be('prefix-job'); + it('returns datafeed-prefix-job from job"', () => { + expect(prefixDatafeedId('job', 'prefix-')).to.be('datafeed-prefix-job'); }); }); diff --git a/x-pack/legacy/plugins/ml/common/util/anomaly_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.d.ts new file mode 100644 index 000000000000..adeb6dc7dd5b --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.d.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. + */ + +import { ANOMALY_SEVERITY } from '../constants/anomalies'; + +export function getSeverity(normalizedScore: number): string; +export function getSeverityType(normalizedScore: number): ANOMALY_SEVERITY; +export function getSeverityColor(normalizedScore: number): string; diff --git a/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js index 371d1d176394..d201a971dda5 100644 --- a/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/anomaly_utils.js @@ -4,25 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ - - /* -* Contains functions for operations commonly performed on anomaly data -* to extract information for display in dashboards. -*/ + * Contains functions for operations commonly performed on anomaly data + * to extract information for display in dashboards. + */ -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule'; import { MULTI_BUCKET_IMPACT } from '../constants/multi_bucket_impact'; +import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../constants/anomalies'; // List of function descriptions for which actual values from record level results should be displayed. -const DISPLAY_ACTUAL_FUNCTIONS = ['count', 'distinct_count', 'lat_long', 'mean', 'max', 'min', 'sum', - 'median', 'varp', 'info_content', 'time']; +const DISPLAY_ACTUAL_FUNCTIONS = [ + 'count', + 'distinct_count', + 'lat_long', + 'mean', + 'max', + 'min', + 'sum', + 'median', + 'varp', + 'info_content', + 'time', +]; // List of function descriptions for which typical values from record level results should be displayed. -const DISPLAY_TYPICAL_FUNCTIONS = ['count', 'distinct_count', 'lat_long', 'mean', 'max', 'min', 'sum', - 'median', 'varp', 'info_content', 'time']; +const DISPLAY_TYPICAL_FUNCTIONS = [ + 'count', + 'distinct_count', + 'lat_long', + 'mean', + 'max', + 'min', + 'sum', + 'median', + 'varp', + 'info_content', + 'time', +]; let severityTypes; @@ -31,26 +51,44 @@ function getSeverityTypes() { return severityTypes; } - return severityTypes = { - critical: { id: 'critical', label: i18n.translate('xpack.ml.anomalyUtils.severity.criticalLabel', { - defaultMessage: 'critical', - }) }, - major: { id: 'major', label: i18n.translate('xpack.ml.anomalyUtils.severity.majorLabel', { - defaultMessage: 'major', - }) }, - minor: { id: 'minor', label: i18n.translate('xpack.ml.anomalyUtils.severity.minorLabel', { - defaultMessage: 'minor', - }) }, - warning: { id: 'warning', label: i18n.translate('xpack.ml.anomalyUtils.severity.warningLabel', { - defaultMessage: 'warning', - }) }, - unknown: { id: 'unknown', label: i18n.translate('xpack.ml.anomalyUtils.severity.unknownLabel', { - defaultMessage: 'unknown', - }) }, - low: { id: 'low', label: i18n.translate('xpack.ml.anomalyUtils.severityWithLow.lowLabel', { - defaultMessage: 'low', - }) }, - }; + return (severityTypes = { + critical: { + id: ANOMALY_SEVERITY.CRITICAL, + label: i18n.translate('xpack.ml.anomalyUtils.severity.criticalLabel', { + defaultMessage: 'critical', + }), + }, + major: { + id: ANOMALY_SEVERITY.MAJOR, + label: i18n.translate('xpack.ml.anomalyUtils.severity.majorLabel', { + defaultMessage: 'major', + }), + }, + minor: { + id: ANOMALY_SEVERITY.MINOR, + label: i18n.translate('xpack.ml.anomalyUtils.severity.minorLabel', { + defaultMessage: 'minor', + }), + }, + warning: { + id: ANOMALY_SEVERITY.WARNING, + label: i18n.translate('xpack.ml.anomalyUtils.severity.warningLabel', { + defaultMessage: 'warning', + }), + }, + unknown: { + id: ANOMALY_SEVERITY.UNKNOWN, + label: i18n.translate('xpack.ml.anomalyUtils.severity.unknownLabel', { + defaultMessage: 'unknown', + }), + }, + low: { + id: ANOMALY_SEVERITY.LOW, + label: i18n.translate('xpack.ml.anomalyUtils.severityWithLow.lowLabel', { + defaultMessage: 'low', + }), + }, + }); } // Returns a severity label (one of critical, major, minor, warning or unknown) @@ -58,34 +96,50 @@ function getSeverityTypes() { export function getSeverity(normalizedScore) { const severityTypesList = getSeverityTypes(); - if (normalizedScore >= 75) { + if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { return severityTypesList.critical; - } else if (normalizedScore >= 50) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { return severityTypesList.major; - } else if (normalizedScore >= 25) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { return severityTypesList.minor; - } else if (normalizedScore >= 0) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { return severityTypesList.warning; } else { return severityTypesList.unknown; } } +export function getSeverityType(normalizedScore) { + if (normalizedScore >= 75) { + return ANOMALY_SEVERITY.CRITICAL; + } else if (normalizedScore >= 50) { + return ANOMALY_SEVERITY.MAJOR; + } else if (normalizedScore >= 25) { + return ANOMALY_SEVERITY.MINOR; + } else if (normalizedScore >= 3) { + return ANOMALY_SEVERITY.WARNING; + } else if (normalizedScore >= 0) { + return ANOMALY_SEVERITY.LOW; + } else { + return ANOMALY_SEVERITY.UNKNOWN; + } +} + // Returns a severity label (one of critical, major, minor, warning, low or unknown) // for the supplied normalized anomaly score (a value between 0 and 100), where scores // less than 3 are assigned a severity of 'low'. export function getSeverityWithLow(normalizedScore) { const severityTypesList = getSeverityTypes(); - if (normalizedScore >= 75) { + if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { return severityTypesList.critical; - } else if (normalizedScore >= 50) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { return severityTypesList.major; - } else if (normalizedScore >= 25) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { return severityTypesList.minor; - } else if (normalizedScore >= 3) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.WARNING) { return severityTypesList.warning; - } else if (normalizedScore >= 0) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { return severityTypesList.low; } else { return severityTypesList.unknown; @@ -95,15 +149,15 @@ export function getSeverityWithLow(normalizedScore) { // Returns a severity RGB color (one of critical, major, minor, warning, low_warning or unknown) // for the supplied normalized anomaly score (a value between 0 and 100). export function getSeverityColor(normalizedScore) { - if (normalizedScore >= 75) { + if (normalizedScore >= ANOMALY_THRESHOLD.CRITICAL) { return '#fe5050'; - } else if (normalizedScore >= 50) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MAJOR) { return '#fba740'; - } else if (normalizedScore >= 25) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.MINOR) { return '#fdec25'; - } else if (normalizedScore >= 3) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.WARNING) { return '#8bc8fb'; - } else if (normalizedScore >= 0) { + } else if (normalizedScore >= ANOMALY_THRESHOLD.LOW) { return '#d2e9f7'; } else { return '#ffffff'; @@ -139,15 +193,15 @@ export function getMultiBucketImpactLabel(multiBucketImpact) { export function getEntityFieldName(record) { // Analyses with by and over fields, will have a top-level by_field_name, but // the by_field_value(s) will be in the nested causes array. - if (_.has(record, 'by_field_name') && _.has(record, 'by_field_value')) { + if (record.by_field_name !== undefined && record.by_field_value !== undefined) { return record.by_field_name; } - if (_.has(record, 'over_field_name')) { + if (record.over_field_name !== undefined) { return record.over_field_name; } - if (_.has(record, 'partition_field_name')) { + if (record.partition_field_name !== undefined) { return record.partition_field_name; } @@ -158,15 +212,15 @@ export function getEntityFieldName(record) { // obtained from Elasticsearch. The function looks first for a by_field, then over_field, // then partition_field, returning undefined if none of these fields are present. export function getEntityFieldValue(record) { - if (_.has(record, 'by_field_value')) { + if (record.by_field_value !== undefined) { return record.by_field_value; } - if (_.has(record, 'over_field_value')) { + if (record.over_field_value !== undefined) { return record.over_field_value; } - if (_.has(record, 'partition_field_value')) { + if (record.partition_field_value !== undefined) { return record.partition_field_value; } @@ -181,7 +235,7 @@ export function getEntityFieldList(record) { entityFields.push({ fieldName: record.partition_field_name, fieldValue: record.partition_field_value, - fieldType: 'partition' + fieldType: 'partition', }); } @@ -189,7 +243,7 @@ export function getEntityFieldList(record) { entityFields.push({ fieldName: record.over_field_name, fieldValue: record.over_field_value, - fieldType: 'over' + fieldType: 'over', }); } @@ -200,7 +254,7 @@ export function getEntityFieldList(record) { entityFields.push({ fieldName: record.by_field_name, fieldValue: record.by_field_value, - fieldType: 'by' + fieldType: 'by', }); } @@ -211,22 +265,24 @@ export function getEntityFieldList(record) { // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. export function showActualForFunction(functionDescription) { - return _.indexOf(DISPLAY_ACTUAL_FUNCTIONS, functionDescription) > -1; + return DISPLAY_ACTUAL_FUNCTIONS.indexOf(functionDescription) > -1; } // Returns whether typical values should be displayed for a record with the specified function description. // Note that the 'function' field in a record contains what the user entered e.g. 'high_count', // whereas the 'function_description' field holds a ML-built display hint for function e.g. 'count'. export function showTypicalForFunction(functionDescription) { - return _.indexOf(DISPLAY_TYPICAL_FUNCTIONS, functionDescription) > -1; + return DISPLAY_TYPICAL_FUNCTIONS.indexOf(functionDescription) > -1; } // Returns whether a rule can be configured against the specified anomaly. export function isRuleSupported(record) { // A rule can be configured with a numeric condition if the function supports it, // and/or with scope if there is a partitioning fields. - return (CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(record.function) === -1) || - (getEntityFieldName(record) !== undefined); + return ( + CONDITIONS_NOT_SUPPORTED_FUNCTIONS.indexOf(record.function) === -1 || + getEntityFieldName(record) !== undefined + ); } // Two functions for converting aggregation type names. @@ -272,5 +328,5 @@ export const aggregationTypeTransform = { } return newAggType; - } + }, }; diff --git a/x-pack/legacy/plugins/ml/common/util/group_color_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/group_color_utils.d.ts new file mode 100644 index 000000000000..4a1a6ebb8fdf --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/util/group_color_utils.d.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 function tabColor(name: string): string; diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts new file mode 100644 index 000000000000..0217ddcdc2cf --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.d.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. + */ + +export interface ValidationMessage { + id: string; +} +export interface ValidationResults { + messages: ValidationMessage[]; + valid: boolean; + contains: (id: string) => boolean; + find: (id: string) => ValidationMessage | undefined; +} +export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: number): number; + +// TODO - use real types for job. Job interface first needs to move to a common location +export function isTimeSeriesViewJob(job: any): boolean; +export function basicJobValidation( + job: any, + fields: any[] | undefined, + limits: any, + skipMmlCheck?: boolean +): ValidationResults; + +export const ML_MEDIAN_PERCENTS: number; + +export const ML_DATA_PREVIEW_COUNT: number; diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.js b/x-pack/legacy/plugins/ml/common/util/job_utils.js index 03d55e9d824b..b3d3e182ee42 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.js @@ -239,7 +239,7 @@ export const ML_DATA_PREVIEW_COUNT = 10; export function prefixDatafeedId(datafeedId, prefix) { return (datafeedId.match(/^datafeed-/)) ? datafeedId.replace(/^datafeed-/, `datafeed-${prefix}`) : - `${prefix}${datafeedId}`; + `datafeed-${prefix}${datafeedId}`; } // Returns a name which is safe to use in elasticsearch aggregations for the supplied diff --git a/x-pack/legacy/plugins/ml/common/util/parse_interval.d.ts b/x-pack/legacy/plugins/ml/common/util/parse_interval.d.ts new file mode 100644 index 000000000000..b3537b94d1c7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/util/parse_interval.d.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. + */ + +interface Duration { + asSeconds(): number; +} +export function parseInterval(interval: string): Duration; diff --git a/x-pack/legacy/plugins/ml/common/util/string_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/string_utils.d.ts new file mode 100644 index 000000000000..f8dbc00643d0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/util/string_utils.d.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 function renderTemplate(str: string, data: string): string; +export function stringHash(str: string): string; diff --git a/x-pack/legacy/plugins/ml/public/app.js b/x-pack/legacy/plugins/ml/public/app.js index 1ac74e9bb632..ca0427544e83 100644 --- a/x-pack/legacy/plugins/ml/public/app.js +++ b/x-pack/legacy/plugins/ml/public/app.js @@ -22,6 +22,7 @@ import 'plugins/ml/jobs'; import 'plugins/ml/services/calendar_service'; import 'plugins/ml/components/messagebar'; import 'plugins/ml/data_frame'; +import 'plugins/ml/data_frame_analytics'; import 'plugins/ml/datavisualizer'; import 'plugins/ml/explorer'; import 'plugins/ml/timeseriesexplorer'; @@ -32,7 +33,6 @@ import 'plugins/ml/components/confirm_modal'; import 'plugins/ml/components/navigation_menu'; import 'plugins/ml/components/loading_indicator'; import 'plugins/ml/settings'; -import 'plugins/ml/file_datavisualizer'; import 'uiExports/autocompleteProviders'; import uiRoutes from 'ui/routes'; diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/annotation_flyout_directive.js b/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/annotation_flyout_directive.js deleted file mode 100644 index 5f766a31b3ae..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/annotation_flyout_directive.js +++ /dev/null @@ -1,45 +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. - */ - - - -/* - * angularjs wrapper directive for the AnnotationsTable React component. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { AnnotationFlyout } from './index'; - -import 'angular'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -import { I18nProvider } from '@kbn/i18n/react'; - -module.directive('mlAnnotationFlyout', function () { - - function link(scope, element) { - ReactDOM.render( - - {React.createElement(AnnotationFlyout)} - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - } - - return { - scope: false, - link: link - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__tests__/annotations_table_directive.js b/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__tests__/annotations_table_directive.js deleted file mode 100644 index d53271aba551..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__tests__/annotations_table_directive.js +++ /dev/null @@ -1,55 +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 jobConfig from '../../../../../common/types/__mocks__/job_config_farequote'; - -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { ml } from '../../../../services/ml_api_service'; - -describe('ML - ', () => { - let $scope; - let $compile; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Plain initialization doesn\'t throw an error', () => { - expect(() => { - $compile('')($scope); - }).to.not.throwError(); - }); - - it('Initialization with empty annotations array doesn\'t throw an error', () => { - expect(() => { - $compile('')($scope); - }).to.not.throwError(); - }); - - it('Initialization with job config doesn\'t throw an error', () => { - const getAnnotationsStub = sinon.stub(ml.annotations, 'getAnnotations').resolves({ annotations: [] }); - - expect(() => { - $scope.jobs = [jobConfig]; - $compile('')($scope); - }).to.not.throwError(); - - getAnnotationsStub.restore(); - }); - -}); diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table_directive.js b/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table_directive.js deleted file mode 100644 index 5e8cece16299..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table_directive.js +++ /dev/null @@ -1,81 +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. - */ - - - -/* - * angularjs wrapper directive for the AnnotationsTable React component. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { AnnotationsTable } from './annotations_table'; - -import 'angular'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -import chrome from 'ui/chrome'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); - -import { I18nContext } from 'ui/i18n'; - -module.directive('mlAnnotationTable', function () { - - function link(scope, element) { - function renderReactComponent() { - if (typeof scope.jobs === 'undefined' && typeof scope.annotations === 'undefined') { - return; - } - - const props = { - annotations: scope.annotations, - jobs: scope.jobs, - isSingleMetricViewerLinkVisible: scope.drillDown, - isNumberBadgeVisible: scope.numberBadge - }; - - ReactDOM.render( - - {React.createElement(AnnotationsTable, props)} - , - element[0] - ); - } - - renderReactComponent(); - - scope.$on('render', () => { - renderReactComponent(); - }); - - function renderFocusChart() { - renderReactComponent(); - } - - if (mlAnnotationsEnabled) { - scope.$watchCollection('annotations', renderFocusChart); - } - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - - } - - return { - scope: { - annotations: '=', - drillDown: '=', - jobs: '=', - numberBadge: '=' - }, - link: link - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/index.js b/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/index.js index 6364275ce2e5..965f201f5e8f 100644 --- a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/index.js +++ b/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/index.js @@ -5,5 +5,3 @@ */ export { AnnotationsTable } from './annotations_table'; - -import './annotations_table_directive'; diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_directive.js b/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_directive.js deleted file mode 100644 index 5772bc98959e..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_directive.js +++ /dev/null @@ -1,31 +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 'ngreact'; - -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from 'ui/modules'; -import { timefilter } from 'ui/timefilter'; -const module = uiModules.get('apps/ml', ['react']); - -import { AnomaliesTable } from './anomalies_table'; - -module.directive('mlAnomaliesTable', function ($injector) { - const reactDirective = $injector.get('reactDirective'); - - return reactDirective( - wrapInI18nContext(AnomaliesTable), - [ - ['filter', { watchDepth: 'reference' }], - ['tableData', { watchDepth: 'reference' }] - ], - { restrict: 'E' }, - { - timefilter - } - ); -}); diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/index.js b/x-pack/legacy/plugins/ml/public/components/anomalies_table/index.js index d16092d1f618..1999654055c7 100644 --- a/x-pack/legacy/plugins/ml/public/components/anomalies_table/index.js +++ b/x-pack/legacy/plugins/ml/public/components/anomalies_table/index.js @@ -5,4 +5,4 @@ */ -import './anomalies_table_directive'; +export { AnomaliesTable } from './anomalies_table'; diff --git a/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts_service.js b/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts_service.js deleted file mode 100644 index c4f12776ad96..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts_service.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 { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { subscribeAppStateToObservable } from '../../../util/app_state_utils'; -import { showCharts$ } from './checkbox_showcharts'; - -module.service('mlCheckboxShowChartsService', function (AppState, $rootScope) { - subscribeAppStateToObservable(AppState, 'mlShowCharts', showCharts$, () => $rootScope.$applyAsync()); -}); diff --git a/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/index.js b/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/index.js index 7c11d47bae2d..b7957b807591 100644 --- a/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/index.js +++ b/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/index.js @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - -import './checkbox_showcharts_service'; +export { CheckboxShowCharts, showCharts$ } from './checkbox_showcharts'; diff --git a/x-pack/legacy/plugins/ml/public/components/controls/index.js b/x-pack/legacy/plugins/ml/public/components/controls/index.js index 275c6e6a6790..26cb89d67263 100644 --- a/x-pack/legacy/plugins/ml/public/components/controls/index.js +++ b/x-pack/legacy/plugins/ml/public/components/controls/index.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './checkbox_showcharts'; -import './select_interval'; -import './select_severity'; +export { CheckboxShowCharts, showCharts$ } from './checkbox_showcharts'; +export { interval$, SelectInterval } from './select_interval'; +export { SelectSeverity, severity$, SEVERITY_OPTIONS } from './select_severity'; diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/__tests__/select_interval_directive.js b/x-pack/legacy/plugins/ml/public/components/controls/select_interval/__tests__/select_interval_directive.js deleted file mode 100644 index 67b39d9bf85f..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/__tests__/select_interval_directive.js +++ /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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import { interval$ } from '../select_interval'; - -describe('ML - mlSelectIntervalService', () => { - let appState; - - beforeEach(ngMock.module('kibana', (stateManagementConfigProvider) => { - stateManagementConfigProvider.enable(); - })); - beforeEach(ngMock.module(($provide) => { - appState = { - fetch: () => {}, - save: () => {} - }; - - $provide.factory('AppState', () => () => appState); - })); - - it('initializes AppState with correct default value', (done) => { - ngMock.inject(($injector) => { - $injector.get('mlSelectIntervalService'); - const defaultValue = { display: 'Auto', val: 'auto' }; - - expect(appState.mlSelectInterval).to.eql(defaultValue); - expect(interval$.getValue()).to.eql(defaultValue); - - done(); - }); - }); - - it('restores AppState to interval$ observable', (done) => { - ngMock.inject(($injector) => { - const restoreValue = { display: '1 day', val: 'day' }; - appState.mlSelectInterval = restoreValue; - - $injector.get('mlSelectIntervalService'); - - expect(appState.mlSelectInterval).to.eql(restoreValue); - expect(interval$.getValue()).to.eql(restoreValue); - - done(); - }); - }); - -}); diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/index.js b/x-pack/legacy/plugins/ml/public/components/controls/select_interval/index.js index 8fe80d63bb99..a38c71d89d07 100644 --- a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/index.js +++ b/x-pack/legacy/plugins/ml/public/components/controls/select_interval/index.js @@ -5,4 +5,4 @@ */ -import './select_interval_directive'; +export { interval$, SelectInterval } from './select_interval'; diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval_directive.js b/x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval_directive.js deleted file mode 100644 index 4391d3395193..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval_directive.js +++ /dev/null @@ -1,27 +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 'ngreact'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { subscribeAppStateToObservable } from '../../../util/app_state_utils'; -import { SelectInterval, interval$ } from './select_interval'; - -module.service('mlSelectIntervalService', function (AppState, $rootScope) { - subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$, () => $rootScope.$applyAsync()); -}) - .directive('mlSelectInterval', function ($injector) { - const reactDirective = $injector.get('reactDirective'); - - return reactDirective( - SelectInterval, - undefined, - { restrict: 'E' } - ); - }); diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/index.js b/x-pack/legacy/plugins/ml/public/components/controls/select_severity/index.js index 618edf599e50..7c841156009f 100644 --- a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/index.js +++ b/x-pack/legacy/plugins/ml/public/components/controls/select_severity/index.js @@ -5,4 +5,4 @@ */ -import './select_severity_directive'; +export { SelectSeverity, severity$, SEVERITY_OPTIONS } from './select_severity'; diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity_directive.js b/x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity_directive.js deleted file mode 100644 index 6ab94225cea4..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity_directive.js +++ /dev/null @@ -1,29 +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 'ngreact'; - -import { wrapInI18nContext } from 'ui/i18n'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { subscribeAppStateToObservable } from '../../../util/app_state_utils'; -import { SelectSeverity, severity$ } from './select_severity'; - -module.service('mlSelectSeverityService', function (AppState, $rootScope) { - subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$, () => $rootScope.$applyAsync()); -}) - .directive('mlSelectSeverity', function ($injector) { - const reactDirective = $injector.get('reactDirective'); - - return reactDirective( - wrapInI18nContext(SelectSeverity), - undefined, - { restrict: 'E' }, - ); - }); diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx new file mode 100644 index 000000000000..6549df35ba38 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx @@ -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 React, { FC } from 'react'; + +import { EuiCard, EuiIcon, IconType } from '@elastic/eui'; + +interface Props { + iconType: IconType; + title: string; + description: string; + onClick(): void; +} + +// Component for rendering a card which links to the Create Job page, displaying an +// icon, card title, description and link. +export const CreateJobLinkCard: FC = ({ iconType, title, description, onClick }) => ( + } + title={title} + description={description} + onClick={onClick} + /> +); diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/index.ts b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/index.ts new file mode 100644 index 000000000000..b0fa3762a4ef --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/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 { CreateJobLinkCard } from './create_job_link_card'; diff --git a/x-pack/legacy/plugins/ml/public/components/display_value/display_value.js b/x-pack/legacy/plugins/ml/public/components/display_value/display_value.js deleted file mode 100644 index b9db4510ca53..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/display_value/display_value.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 { - EuiToolTip -} from '@elastic/eui'; - - -const MAX_CHARS = 12; - -export function DisplayValue({ value }) { - const length = String(value).length; - let formattedValue; - - if (length <= MAX_CHARS) { - formattedValue = value; - } else { - formattedValue = ( - - - {value} - - - ); - } - - return formattedValue; -} diff --git a/x-pack/legacy/plugins/ml/public/components/display_value/display_value.tsx b/x-pack/legacy/plugins/ml/public/components/display_value/display_value.tsx new file mode 100644 index 000000000000..cfe3d09a1632 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/display_value/display_value.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, { FC } from 'react'; +import { EuiToolTip } from '@elastic/eui'; + +const MAX_CHARS = 12; + +export const DisplayValue: FC<{ value: any }> = ({ value }) => { + const length = String(value).length; + + if (length <= MAX_CHARS) { + return value; + } else { + return ( + + {value} + + ); + } +}; diff --git a/x-pack/legacy/plugins/ml/public/components/display_value/index.js b/x-pack/legacy/plugins/ml/public/components/display_value/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/display_value/index.js rename to x-pack/legacy/plugins/ml/public/components/display_value/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/_field_data_card.scss b/x-pack/legacy/plugins/ml/public/components/field_data_card/_field_data_card.scss deleted file mode 100644 index 5f1e480c7ce2..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/_field_data_card.scss +++ /dev/null @@ -1,186 +0,0 @@ -// SASSTODO: This entire sass file needs to be rewritten, using a color blind viz palette and proper vars. -// This will need to be done in a more thorough cleanup. - -.ml-field-data-card { - width: 360px; - height: 435px; - - .boolean { - background-color: #e6c220; - } - - .date { - background-color: #f98510; - } - - .document_count { - background-color: #db1374; - } - - .geo_point { - background-color: #461a0a; - } - - .ip { - background-color: #490092; - } - - .keyword { - background-color: #00b3a4; - } - - .number { - background-color: #3185fc; - } - - .text { - background-color: #920000; - } - - .type-other, .unknown { - background-color: #bfa180; - } - - // Use euiPanel styling - @include euiPanel($selector: 'card-contents'); - - .card-contents { - height: 400px; - border-radius: 0px 0px $euiBorderRadius $euiBorderRadius; - overflow: hidden; - } - - .stats { - padding: 10px 10px 0px 10px; - text-align: center; - } - - .stat { - padding-bottom: 6px; - } - - .stat.heading { - padding-bottom: 0px; - } - - .stat.min, .stat.max, .stat.median { - width: 30%; - display: inline-block; - } - - .stat.min.value, .stat.max.value, .stat.median.value { - font-size: $euiFontSizeS; - @include euiTextTruncate; - } - - .valueWrapper { - display: inline; - } - - .not-exist-message { - padding: 50px 30px 0px 30px; - text-align: center; - } - - .sampled-message { - font-size: 11px; - color: #555555; - text-align: center; - padding-top: 3px; - } - - .text-code { - font-family: $euiCodeFontFamily; - } - - .details-select { - text-align: center; - margin-top: 5px; - margin-bottom: 5px; - } - - .details-container { - padding-top: 5px; - } - - svg { - font-size: 11px; - font-family: $euiFontFamily; - - text { - fill: $euiColorDarkShade; - } - - .info-text { - text-anchor: middle; - } - - .distribution-chart { - - .x.axis path, .x.axis line { - fill: none; - stroke: #cccccc; - shape-rendering: crispEdges; - } - - .area { - fill: #3185fc; - } - } - - } - - ml-metric-distribution-chart { - display: block; - padding: 0px 15px 10px 15px; - } - - ml-document-count-chart { - display: block; - padding: 18px 15px 0px 15px; - rect.bar { - fill: #db1374; - } - - .bar { - stroke: #ffffff; - } - - .axis path, .axis line { - fill: none; - stroke: #cccccc; - shape-rendering: crispEdges; - } - } - - .top-value { - height: 21px; - font-size: 13px; - - .field-label { - @include euiTextTruncate; - - display: inline-block; - width: 100px; - text-align: right; - } - - .count-label { - display: inline-block; - width: 70px; - text-align: left; - overflow: hidden; - text-overflow: ellipsis; - } - - .top-value-bar-holder { - display: inline-block; - width: 160px; - } - - .top-value-bar { - height: 15px; - min-width: 3px; - } - } -} diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/_index.scss b/x-pack/legacy/plugins/ml/public/components/field_data_card/_index.scss deleted file mode 100644 index c39be8d5f17d..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'field_data_card'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_boolean.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_boolean.html deleted file mode 100644 index 312d3600436d..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_boolean.html +++ /dev/null @@ -1,56 +0,0 @@ -
    -
    -
    - - ({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%) -
    -
    - -
    -
    -
    -
    - true -
    -
    -
    -
    -
    -
    {{card.stats.trueCount}}
    -
    -
    -
    - false -
    -
    -
    -
    -
    -
    {{card.stats.falseCount}}
    -
    -
    -
    diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_date.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_date.html deleted file mode 100644 index 6841252132cf..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_date.html +++ /dev/null @@ -1,30 +0,0 @@ -
    -
    -
    - - ({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%) -
    -
    -
    -
    -
    diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_document_count.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_document_count.html deleted file mode 100644 index ab157a5f2319..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_document_count.html +++ /dev/null @@ -1,17 +0,0 @@ -
    -
    - -
    - -
    -
    -
    - diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_geo_point.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_geo_point.html deleted file mode 100644 index 21005c449ab4..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_geo_point.html +++ /dev/null @@ -1,36 +0,0 @@ -
    -
    -
    - - ({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%) -
    -
    - -
    -
    -
    -
    -
    - {{ example }} -
    -
    -
    diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_ip.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_ip.html deleted file mode 100644 index a6e8c7b47a9c..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_ip.html +++ /dev/null @@ -1,33 +0,0 @@ -
    -
    -
    - - ({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%) -
    -
    - -
    - -
    - -
    -
    - -
    -
    diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_keyword.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_keyword.html deleted file mode 100644 index 750ee2a04c94..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_keyword.html +++ /dev/null @@ -1,32 +0,0 @@ -
    -
    -
    - - ({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%) -
    -
    - -
    -
    - -
    -
    - -
    -
    diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_number.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_number.html deleted file mode 100644 index 024ab88b0bef..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_number.html +++ /dev/null @@ -1,80 +0,0 @@ -
    -
    -
    - - ({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%) -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_other.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_other.html deleted file mode 100644 index dacbf4091eb4..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_other.html +++ /dev/null @@ -1,44 +0,0 @@ -
    -
    -
    -
    - - ({{ 100 * card.stats.count / card.stats.sampleCount | number:1 }}%) -
    -
    - -
    -
    -
    -
    -
    - {{ example }} -
    -
    -
    diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_text.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_text.html deleted file mode 100644 index 5d28f05a9abe..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/content_types/card_text.html +++ /dev/null @@ -1,44 +0,0 @@ -
    -
    -
    -
    - {{ example }} -
    -
    -
    -
    -

    -

    -

    -
    -
    - -
    diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/display_value_directive.js b/x-pack/legacy/plugins/ml/public/components/field_data_card/display_value_directive.js deleted file mode 100644 index f94e721319c2..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/display_value_directive.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 'ngreact'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { DisplayValue } from '../../components/display_value'; - -module.directive('mlDisplayValue', function (reactDirective) { - return reactDirective( - DisplayValue, - undefined, - { restrict: 'E' } - ); -}); diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/document_count_chart_directive.js b/x-pack/legacy/plugins/ml/public/components/field_data_card/document_count_chart_directive.js deleted file mode 100644 index 1550d8077afd..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/document_count_chart_directive.js +++ /dev/null @@ -1,190 +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. - */ - - - -/* - * AngularJS directive for rendering a chart showing - * document count on the field data card. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import d3 from 'd3'; -import moment from 'moment'; - -import { parseInterval } from 'ui/utils/parse_interval'; -import { numTicksForDateFormat } from '../../util/chart_utils'; -import { calculateTextWidth } from '../../util/string_utils'; -import { MlTimeBuckets } from '../../util/ml_time_buckets'; -import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; -import { formatHumanReadableDateTime } from '../../util/date_utils'; - -import { uiModules } from 'ui/modules'; -import { timefilter } from 'ui/timefilter'; -const module = uiModules.get('apps/ml'); - -module.directive('mlDocumentCountChart', function () { - function link(scope, element, attrs) { - const svgWidth = attrs.width ? +attrs.width : 400; - const svgHeight = scope.height = attrs.height ? +attrs.height : 400; - - const margin = { top: 0, right: 5, bottom: 20, left: 15 }; - - let chartWidth = svgWidth - (margin.left + margin.right); - const chartHeight = svgHeight - (margin.top + margin.bottom); - - let xScale = null; - let yScale = d3.scale.linear().range([chartHeight, 0]); - let xAxisTickFormat = 'YYYY-MM-DD HH:mm'; - - let barChartGroup; - let barWidth = 5; // Adjusted according to data aggregation interval. - - scope.chartData = []; - - element.on('$destroy', function () { - scope.$destroy(); - }); - - function processChartData() { - // Build the dataset in format used by the d3 chart i.e. array - // of Objects with keys time (epoch ms), date (JavaScript date) and value. - const bucketsData = _.get(scope, ['card', 'stats', 'documentCounts', 'buckets'], {}); - const chartData = []; - _.each(bucketsData, (value, time) => { - chartData.push({ - date: new Date(+time), - time: +time, - value - }); - }); - - scope.chartData = chartData; - } - - function render() { - // Clear any existing elements from the visualization, - // then build the svg elements for the bar chart. - const chartElement = d3.select(element.get(0)).select('.content-wrapper'); - chartElement.selectAll('*').remove(); - - if (scope.chartData === undefined) { - return; - } - - const svg = chartElement.append('svg') - .attr('width', svgWidth) - .attr('height', svgHeight); - - // Set the size of the left margin according to the width - // of the largest y axis tick label. - const maxYVal = d3.max(scope.chartData, (d) => d.value); - yScale = yScale.domain([0, maxYVal]); - - const yAxis = d3.svg.axis().scale(yScale).orient('left').outerTickSize(0); - - // barChartGroup translate doesn't seem to be relative - // to parent svg, so have to add an extra 5px on. - const maxYAxisLabelWidth = calculateTextWidth(maxYVal, true, svg); - margin.left = Math.max(maxYAxisLabelWidth + yAxis.tickPadding() + 5, 25); - chartWidth = Math.max(svgWidth - margin.left - margin.right, 0); - - const bounds = timefilter.getActiveBounds(); - xScale = d3.time.scale() - .domain([new Date(bounds.min.valueOf()), new Date(bounds.max.valueOf())]) - .range([0, chartWidth]); - - if (scope.chartData.length > 0) { - // x axis tick format and bar width determined by data aggregation interval. - const buckets = new MlTimeBuckets(); - const aggInterval = _.get(scope, ['card', 'stats', 'documentCounts', 'interval']); - buckets.setInterval(aggInterval); - buckets.setBounds(bounds); - xAxisTickFormat = buckets.getScaledDateFormat(); - - const intervalMs = parseInterval(aggInterval).asMilliseconds(); - barWidth = xScale(scope.chartData[0].time + intervalMs) - xScale(scope.chartData[0].time); - } - - const xAxis = d3.svg.axis().scale(xScale).orient('bottom') - .outerTickSize(0).ticks(numTicksForDateFormat(chartWidth, xAxisTickFormat)) - .tickFormat((d) => { - return moment(d).format(xAxisTickFormat); - }); - - barChartGroup = svg.append('g') - .attr('class', 'bar-chart') - .attr('transform', `translate(${margin.left}, ${margin.top})`); - - drawBarChartAxes(xAxis, yAxis); - drawBarChartPaths(); - } - - function drawBarChartAxes(xAxis, yAxis) { - const axes = barChartGroup.append('g'); - - axes.append('g') - .attr('class', 'x axis') - .attr('transform', `translate(0, ${chartHeight})`) - .call(xAxis); - - axes.append('g') - .attr('class', 'y axis') - .call(yAxis); - } - - function drawBarChartPaths() { - barChartGroup.selectAll('bar') - .data(scope.chartData) - .enter().append('rect') - .attr('class', 'bar') - .attr('x', (d) => { return xScale(d.time); }) - .attr('width', barWidth) - .attr('y', (d) => { return yScale(d.value); }) - .attr('height', (d) => { return chartHeight - yScale(d.value); }) - .on('mouseover', function (d) { - showChartTooltip(d, this); - }) - .on('mousemove', function (d) { - showChartTooltip(d, this); - }) - .on('mouseout', () => mlChartTooltipService.hide()); - - function showChartTooltip(data, rect) { - const formattedDate = formatHumanReadableDateTime(data.time); - const contents = i18n.translate('xpack.ml.fieldDataCard.documentCountChart.chartTooltip', { - defaultMessage: '{formattedDate}{br}{hr}count: {dataValue}', - values: { - formattedDate, - dataValue: data.value, - br: '
    ', - hr: '
    ', - }, - }); - - // Calculate the y offset. - // rectY are mouseY are relative to top of the chart area. - const rectY = d3.select(rect).attr('y'); - const mouseY = +(d3.mouse(rect)[1]); - - mlChartTooltipService.show(contents, rect, { - x: 5, - y: (mouseY - rectY) - }); - } - } - - // Process the data and then render the chart. - processChartData(); - render(); - } - - return { - scope: false, - link: link - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/field_data_card.html b/x-pack/legacy/plugins/ml/public/components/field_data_card/field_data_card.html deleted file mode 100644 index 4fbe5e465eed..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/field_data_card.html +++ /dev/null @@ -1,24 +0,0 @@ -
    - - -
    - -
    - -
    -
    -
    - - -
    diff --git a/x-pack/legacy/plugins/ml/public/components/field_data_card/field_data_card_directive.js b/x-pack/legacy/plugins/ml/public/components/field_data_card/field_data_card_directive.js deleted file mode 100644 index ff3a7b6238ae..000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_data_card/field_data_card_directive.js +++ /dev/null @@ -1,114 +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. - */ - - - -/* - * AngularJS directive for rendering a card showing data on a field in an index pattern. - */ - -import _ from 'lodash'; -import $ from 'jquery'; -import moment from 'moment'; -import chrome from 'ui/chrome'; - -import template from './field_data_card.html'; -import { ML_JOB_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types'; -import { mlEscape } from 'plugins/ml/util/string_utils'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module.directive('mlFieldDataCard', function (config) { - - function link(scope, element) { - scope.ML_JOB_FIELD_TYPES = ML_JOB_FIELD_TYPES; - scope.mlEscape = mlEscape; - - if (scope.card.type === ML_JOB_FIELD_TYPES.NUMBER) { - if (scope.card.fieldName) { - scope.$watch('card.stats', () => { - const cardinality = _.get(scope, ['card', 'stats', 'cardinality'], 0); - scope.detailsMode = cardinality > 100 ? 'distribution' : 'top'; - }); - - const cardinality = _.get(scope, ['card', 'stats', 'cardinality'], 0); - scope.detailsMode = cardinality > 100 ? 'distribution' : 'top'; - } - // Create a div for the chart tooltip. - $('.ml-field-data-card-tooltip').remove(); - $('body').append('