Merge branch 'master' of github.com:elastic/kibana into feature-ingest
|
@ -214,13 +214,6 @@ module.exports = {
|
|||
'jsx-a11y/click-events-have-key-events': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['x-pack/legacy/plugins/siem/**/*.{js,ts,tsx}'],
|
||||
rules: {
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['x-pack/legacy/plugins/snapshot_restore/**/*.{js,ts,tsx}'],
|
||||
rules: {
|
||||
|
@ -839,6 +832,8 @@ module.exports = {
|
|||
// might be introduced after the other warns are fixed
|
||||
// 'react/jsx-sort-props': 'error',
|
||||
'react/jsx-tag-spacing': 'error',
|
||||
// might be introduced after the other warns are fixed
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'require-atomic-updates': 'error',
|
||||
'rest-spread-spacing': ['error', 'never'],
|
||||
'symbol-description': 'error',
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
"dashboardEmbeddableContainer": "src/plugins/dashboard_embeddable_container",
|
||||
"data": ["src/legacy/core_plugins/data", "src/plugins/data"],
|
||||
"embeddableApi": "src/plugins/embeddable",
|
||||
"share": "src/plugins/share",
|
||||
"esUi": "src/plugins/es_ui_shared",
|
||||
"expressions_np": "src/plugins/expressions",
|
||||
"expressions": "src/legacy/core_plugins/expressions",
|
||||
"expressions": "src/plugins/expressions",
|
||||
"inputControl": "src/legacy/core_plugins/input_control_vis",
|
||||
"inspector": "src/plugins/inspector",
|
||||
"inspectorViews": "src/legacy/core_plugins/inspector_views",
|
||||
|
@ -34,7 +34,7 @@
|
|||
"visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud",
|
||||
"visTypeTimeseries": "src/legacy/core_plugins/vis_type_timeseries",
|
||||
"visTypeVega": "src/legacy/core_plugins/vis_type_vega",
|
||||
"visualizations": "src/plugins/visualizations"
|
||||
"visualizations": ["src/plugins/visualizations", "src/legacy/core_plugins/visualizations"]
|
||||
},
|
||||
"exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"],
|
||||
"translations": []
|
||||
|
|
|
@ -325,7 +325,7 @@ Note that for VSCode, to enable "live" linting of TypeScript (and other) file ty
|
|||
"eslint.autoFixOnSave": true,
|
||||
```
|
||||
|
||||
It is **not** recommended to use `prettier` plugin on Kibana project. Because settings are in `eslintrc.js` file and it is applied to too many files that shouldn't be prettier-ized.
|
||||
:warning: It is **not** recommended to use the [`Prettier` extension/IDE plugin](https://prettier.io/) while maintaining the Kibana project. Formatting and styling roles are set in the multiple `.eslintrc.js` files across the project and some of them use the [NPM version of Prettier](https://www.npmjs.com/package/prettier). Using the IDE extension might cause conflicts, applying the formatting to too many files that shouldn't be prettier-ized and/or highlighting errors that are actually OK.
|
||||
|
||||
### Internationalization
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ image::images/canvas-create-URL.gif[Create POST URL]
|
|||
[[add-workpad-website]]
|
||||
=== Share the workpad on a website
|
||||
|
||||
beta[] Download the workpad and share it on any website, then customize the workpad behavior to autoplay the pages or hide the toolbar.
|
||||
beta[] Canvas allows you to create _shareables_, which are workpads that you download and securely share on any website. To customize the behavior of the workpad on your website, you can choose to autoplay the pages or hide the workpad toolbar.
|
||||
|
||||
. If you are using a Gold or Platinum license, enable reporting in your `config/kibana.yml` file.
|
||||
|
||||
|
@ -74,7 +74,7 @@ NOTE: Shareable workpads encode the current state of the workpad in a JSON file.
|
|||
|
||||
[float]
|
||||
[[change-the-workpad-settings]]
|
||||
=== Change the shareable workpad settings
|
||||
=== Change the settings
|
||||
|
||||
After you've added the workpad to your website, you can change the autoplay and toolbar settings.
|
||||
|
||||
|
|
|
@ -15,11 +15,6 @@ export interface ChromeNavControl
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [mount](./kibana-plugin-public.chromenavcontrol.mount.md) | <code>MountPoint</code> | |
|
||||
| [order](./kibana-plugin-public.chromenavcontrol.order.md) | <code>number</code> | |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [mount(targetDomElement)](./kibana-plugin-public.chromenavcontrol.mount.md) | |
|
||||
|
||||
|
|
|
@ -2,21 +2,10 @@
|
|||
|
||||
[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavControl](./kibana-plugin-public.chromenavcontrol.md) > [mount](./kibana-plugin-public.chromenavcontrol.mount.md)
|
||||
|
||||
## ChromeNavControl.mount() method
|
||||
## ChromeNavControl.mount property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
mount(targetDomElement: HTMLElement): () => void;
|
||||
mount: MountPoint;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| targetDomElement | <code>HTMLElement</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`() => void`
|
||||
|
||||
|
|
|
@ -9,5 +9,5 @@ A function that should mount DOM content inside the provided container element a
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type MountPoint = (element: HTMLElement) => UnmountCallback;
|
||||
export declare type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
|
||||
```
|
||||
|
|
|
@ -16,6 +16,6 @@ export interface OverlayStart
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [banners](./kibana-plugin-public.overlaystart.banners.md) | <code>OverlayBannersStart</code> | [OverlayBannersStart](./kibana-plugin-public.overlaybannersstart.md) |
|
||||
| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | <code>(flyoutChildren: React.ReactNode, flyoutProps?: {</code><br/><code> closeButtonAriaLabel?: string;</code><br/><code> 'data-test-subj'?: string;</code><br/><code> }) => OverlayRef</code> | |
|
||||
| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | <code>(modalChildren: React.ReactNode, modalProps?: {</code><br/><code> className?: string;</code><br/><code> closeButtonAriaLabel?: string;</code><br/><code> 'data-test-subj'?: string;</code><br/><code> }) => OverlayRef</code> | |
|
||||
| [openFlyout](./kibana-plugin-public.overlaystart.openflyout.md) | <code>OverlayFlyoutStart['open']</code> | |
|
||||
| [openModal](./kibana-plugin-public.overlaystart.openmodal.md) | <code>OverlayModalStart['open']</code> | |
|
||||
|
||||
|
|
|
@ -4,11 +4,9 @@
|
|||
|
||||
## OverlayStart.openFlyout property
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
}) => OverlayRef;
|
||||
openFlyout: OverlayFlyoutStart['open'];
|
||||
```
|
||||
|
|
|
@ -4,12 +4,9 @@
|
|||
|
||||
## OverlayStart.openModal property
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
openModal: (modalChildren: React.ReactNode, modalProps?: {
|
||||
className?: string;
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
}) => OverlayRef;
|
||||
openModal: OverlayModalStart['open'];
|
||||
```
|
||||
|
|
|
@ -21,7 +21,7 @@ Each route can have only one handler function, which is executed when the route
|
|||
|
||||
```ts
|
||||
const router = createRouter();
|
||||
// handler is called when '${my-plugin-id}/path' resource is requested with `GET` method
|
||||
// handler is called when '/path' resource is requested with `GET` method
|
||||
router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' }));
|
||||
|
||||
```
|
||||
|
|
|
@ -27,7 +27,7 @@ export interface HttpServiceSetup
|
|||
|
||||
## Example
|
||||
|
||||
To handle an incoming request in your plugin you should: - Create a `Router` instance. Router is already configured to use `plugin-id` to prefix path segment for your routes.
|
||||
To handle an incoming request in your plugin you should: - Create a `Router` instance.
|
||||
|
||||
```ts
|
||||
const router = httpSetup.createRouter();
|
||||
|
@ -61,7 +61,7 @@ const handler = async (context: RequestHandlerContext, request: KibanaRequest, r
|
|||
}
|
||||
|
||||
```
|
||||
- Register route handler for GET request to 'my-app/path/<!-- -->{<!-- -->id<!-- -->}<!-- -->' path
|
||||
- Register route handler for GET request to 'path/<!-- -->{<!-- -->id<!-- -->}<!-- -->' path
|
||||
|
||||
```ts
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
|
|
@ -73,7 +73,7 @@ set these terms will be matched against all fields. For example, a query for `re
|
|||
in the response field, but a query for just `200` will search for 200 across all fields in your index.
|
||||
============
|
||||
|
||||
===== Nested Field Support
|
||||
==== Nested Field Support
|
||||
|
||||
KQL supports querying on {ref}/nested.html[nested fields] through a special syntax. You can query nested fields in subtly different
|
||||
ways, depending on the results you want, so crafting nested queries requires extra thought.
|
||||
|
@ -85,7 +85,8 @@ There are two main approaches to take:
|
|||
* *Parts of the query can match different nested documents.* This is how a regular object field works.
|
||||
Although generally less useful, there might be occasions where you want to query a nested field in this way.
|
||||
|
||||
Let's take a look at the first approach. In the following document, `items` is a nested field:
|
||||
Let's take a look at the first approach. In the following document, `items` is a nested field. Each document in the nested
|
||||
field contains a name, stock, and category.
|
||||
|
||||
[source,json]
|
||||
----------------------------------
|
||||
|
@ -116,21 +117,38 @@ Let's take a look at the first approach. In the following document, `items` is a
|
|||
}
|
||||
----------------------------------
|
||||
|
||||
===== Match a single nested document
|
||||
|
||||
To find stores that have more than 10 bananas in stock, you would write a query like this:
|
||||
|
||||
`items:{ name:banana and stock > 10 }`
|
||||
|
||||
`items` is the "nested path". Everything inside the curly braces (the "nested group") must match a single document.
|
||||
For example, `items:{ name:banana and stock:9 }` does not match because there isn't a single nested document that
|
||||
matches the entire query in the nested group.
|
||||
`items` is the "nested path". Everything inside the curly braces (the "nested group") must match a single nested document.
|
||||
|
||||
What if you want to find a store with more than 10 bananas that *also* stocks vegetables? This is the second way of querying a nested field, and you can do it like this:
|
||||
The following example returns no matches because no single nested document has bananas with a stock of 9.
|
||||
|
||||
`items:{ name:banana and stock:9 }`
|
||||
|
||||
==== Match different nested documents
|
||||
|
||||
The subqueries in this example are in separate nested groups and can match different nested documents.
|
||||
|
||||
`items:{ name:banana } and items:{ stock:9 }`
|
||||
|
||||
`name:banana` matches the first document in the array and `stock:9` matches the third document in the array.
|
||||
|
||||
==== Combine approaches
|
||||
|
||||
You can combine these two approaches to create complex queries. What if you wanted to find a store with more than 10
|
||||
bananas that *also* stocks vegetables? You could do this:
|
||||
|
||||
`items:{ name:banana and stock > 10 } and items:{ category:vegetable }`
|
||||
|
||||
The first nested group (`name:banana and stock > 10`) must still match a single document, but the `category:vegetables`
|
||||
subquery can match a different nested document because it is in a separate group.
|
||||
|
||||
==== Nested fields inside other nested fields
|
||||
|
||||
KQL's syntax also supports nested fields inside of other nested fields—you simply have to specify the full path. Suppose you
|
||||
have a document where `level1` and `level2` are both nested fields:
|
||||
|
||||
|
|
BIN
docs/images/lens_data_info.gif
Normal file
After Width: | Height: | Size: 4.9 MiB |
BIN
docs/images/lens_drag_drop.gif
Normal file
After Width: | Height: | Size: 42 MiB |
BIN
docs/images/lens_remove_layer.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
docs/images/lens_suggestions.gif
Normal file
After Width: | Height: | Size: 28 MiB |
BIN
docs/images/lens_tutorial_1.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
docs/images/lens_tutorial_2.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
docs/images/lens_tutorial_3.png
Normal file
After Width: | Height: | Size: 167 KiB |
|
@ -2,7 +2,7 @@
|
|||
[role="xpack"]
|
||||
== Managing {beats}
|
||||
|
||||
beta[]
|
||||
include::{asciidoc-dir}/../../shared/discontinued.asciidoc[tag=cm-discontinued]
|
||||
|
||||
Use the Central Management UI under *Management > {beats}* to define and
|
||||
manage configurations in a central location in {kib} and quickly deploy
|
||||
|
|
|
@ -14,7 +14,7 @@ You can create a layer that requests data from {es} from the following:
|
|||
|
||||
** Grid aggregation source
|
||||
|
||||
** <<terms-join>>. The search context is applied to both the terms join and the vector source when the vector source is provided by Elasticsearch documents.
|
||||
** <<terms-join>>
|
||||
|
||||
* <<heatmap-layer>> with Grid aggregation source
|
||||
|
||||
|
@ -87,8 +87,11 @@ The most common cause for empty layers are searches for a field that exists in o
|
|||
[[maps-disable-search-for-layer]]
|
||||
==== Disable search for layer
|
||||
|
||||
To prevent the global search bar from applying search context to a layer, clear the *Apply global filter to layer* checkbox in Layer settings.
|
||||
Disabling the search context applies to the layer source and all <<terms-join, term joins>> configured for the layer.
|
||||
You can prevent the search bar from applying search context to a layer by configuring the following:
|
||||
|
||||
* In *Source settings*, clear the *Apply global filter to source* checkbox to turn off the global search context for the layer source.
|
||||
|
||||
* In *Term joins*, clear the *Apply global filter to join* checkbox to turn off the global search context for the <<terms-join, term join>>.
|
||||
|
||||
[float]
|
||||
[[maps-add-index-search]]
|
||||
|
|
|
@ -14,9 +14,7 @@ However, these docs will be kept up-to-date to reflect the current implementatio
|
|||
[float]
|
||||
[[reporting-nav-bar-extensions]]
|
||||
=== Share menu extensions
|
||||
X-Pack uses the `ShareContextMenuExtensionsRegistryProvider` to register actions in the share menu.
|
||||
|
||||
This integration will likely be changing in the near future as we move towards a unified actions abstraction across {kib}.
|
||||
X-Pack uses the `share` plugin of the Kibana platform to register actions in the share menu.
|
||||
|
||||
[float]
|
||||
=== Generate job URL
|
||||
|
|
|
@ -24,9 +24,10 @@ To create a visualization:
|
|||
. Click on *Visualize* in the side navigation.
|
||||
. Click the *Create new visualization* button or the **+** button.
|
||||
. Choose the visualization type:
|
||||
+
|
||||
|
||||
* *Basic charts*
|
||||
[horizontal]
|
||||
<<lens,Lens>>:: Quickly build several types of basic visualizations by simply dragging and dropping the data fields you want to display.
|
||||
<<xy-chart,Line, Area and Bar charts>>:: Compare different series in X/Y charts.
|
||||
<<heatmap-chart,Heat maps>>:: Shade cells within a matrix.
|
||||
<<pie-chart,Pie chart>>:: Display each source's contribution to a total.
|
||||
|
@ -142,6 +143,8 @@ include::{kib-repo-dir}/visualize/saving.asciidoc[]
|
|||
|
||||
include::{kib-repo-dir}/visualize/visualize_rollup_data.asciidoc[]
|
||||
|
||||
include::{kib-repo-dir}/visualize/lens.asciidoc[]
|
||||
|
||||
include::{kib-repo-dir}/visualize/xychart.asciidoc[]
|
||||
|
||||
include::{kib-repo-dir}/visualize/controls.asciidoc[]
|
||||
|
|
186
docs/visualize/lens.asciidoc
Normal file
|
@ -0,0 +1,186 @@
|
|||
[role="xpack"]
|
||||
[[lens]]
|
||||
== Lens
|
||||
|
||||
beta[]
|
||||
|
||||
*Lens* provides you with a simple and fast way to create visualizations from your Elasticsearch data. With Lens, you can:
|
||||
|
||||
* Quickly build visualizations by dragging and dropping data fields.
|
||||
|
||||
* Understand your data with a summary view on each field.
|
||||
|
||||
* Easily change the visualization type by selecting the automatically generated visualization suggestions.
|
||||
|
||||
* Save your visualization for use in a dashboard.
|
||||
|
||||
[float]
|
||||
[[drag-drop]]
|
||||
=== Drag and drop
|
||||
|
||||
The data panel in the left column shows the data fields for the selected time period. When
|
||||
you drag a field from the data panel, Lens highlights where you can drop that field. The first time you drag a data field,
|
||||
you'll see two places highlighted in green:
|
||||
|
||||
* The visualization builder pane
|
||||
|
||||
* The *X-axis* or *Y-axis* fields in the right column
|
||||
|
||||
You can incorporate many fields into your visualization, and Lens uses heuristics to decide how
|
||||
to apply each one to the visualization.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/lens_drag_drop.gif[]
|
||||
|
||||
TIP: Drag-and-drop capabilities are available only when Lens knows how to use the data. You can still customize
|
||||
your visualization if Lens is unable to make a suggestion.
|
||||
|
||||
[float]
|
||||
[[apply-lens-filters]]
|
||||
==== Find the right data
|
||||
|
||||
Lens shows you fields based on the <<index-patterns, index patterns>> you have defined in
|
||||
{kib}, and the current time range. When you change the index pattern or time filter,
|
||||
the list of fields are updated.
|
||||
|
||||
To narrow the list of fields you see in the left panel, you can:
|
||||
|
||||
* Enter the field name in *Search field names*.
|
||||
|
||||
* Click *Filter by type*, then select the filter. You can also select *Only show fields with data*
|
||||
to show the full list of fields from the index pattern.
|
||||
|
||||
[float]
|
||||
[[view-data-summaries]]
|
||||
==== Data summaries
|
||||
|
||||
To help you decide exactly the data you want to display, get a quick summary of each data field.
|
||||
The summary shows the distribution of values in the time range.
|
||||
|
||||
To view the data information, navigate to a data field, then click *i*.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/lens_data_info.gif[]
|
||||
|
||||
[float]
|
||||
[[change-the-visualization-type]]
|
||||
==== Change the visualization type
|
||||
|
||||
With Lens, you are no longer required to build each visualization from scratch. Lens allows
|
||||
you to switch between any supported chart type at any time. Lens also provides
|
||||
suggestions, which are shortcuts to alternate visualizations based on the data you have.
|
||||
|
||||
You can switch between suggestions without losing your previous state:
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/lens_suggestions.gif[]
|
||||
|
||||
If you want to switch to a chart type that is not suggested, click the chart type in the
|
||||
top right, then select a chart type. When there is an exclamation point (!)
|
||||
next to a chart type, Lens is unable to transfer your current data, but
|
||||
still allows you to make the change.
|
||||
|
||||
[float]
|
||||
[[customize-operation]]
|
||||
==== Customize the data for your visualization
|
||||
|
||||
Lens allows some customizations of the data for each visualization.
|
||||
|
||||
. Change the index pattern.
|
||||
|
||||
.. In the left column, click the index pattern name.
|
||||
|
||||
.. Select the new index pattern.
|
||||
+
|
||||
If there is a match, Lens displays the new data. All fields that do not match the index pattern are removed.
|
||||
|
||||
. Change the data field options, such as the aggregation or label.
|
||||
|
||||
.. Click *Drop a field here* or the field name in the right column.
|
||||
|
||||
.. Change the options that appear depending on the type of field.
|
||||
|
||||
[float]
|
||||
[[layers]]
|
||||
==== Layers in bar, line, and area charts
|
||||
|
||||
The bar, line, and area charts allow you to layer two different series. To add a layer, click *+*.
|
||||
|
||||
To remove a layer, click the chart icon next to the index name:
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/lens_remove_layer.png[]
|
||||
|
||||
[float]
|
||||
[[lens-tutorial]]
|
||||
=== Lens tutorial
|
||||
|
||||
Ready to create your own visualization with Lens? Use the following tutorial to create a visualization that
|
||||
lets you compare sales over time.
|
||||
|
||||
[float]
|
||||
[[lens-before-begin]]
|
||||
==== Before you begin
|
||||
|
||||
To start, you'll need to add the <<add-sample-data, sample ecommerce data>>.
|
||||
|
||||
[float]
|
||||
==== Build the visualization
|
||||
|
||||
Drag and drop your data onto the visualization builder pane.
|
||||
|
||||
. Open *Visualize*, then click *Create visualization*.
|
||||
|
||||
. On the *New Visualization* window, click *Lens*.
|
||||
|
||||
. In the left column, select the *kibana_sample_data_ecommerce* index.
|
||||
|
||||
. Click image:images/time-filter-calendar.png[], then click *Last 7 days*. The list of data fields are updated.
|
||||
|
||||
. Drag and drop the *taxful_total_price* data field to the visualization builder pane.
|
||||
+
|
||||
[role="screenshot"]
|
||||
image::images/lens_tutorial_1.png[Lens tutorial]
|
||||
|
||||
Lens has taken your intent to see *taxful_total_price* and added in the *order_date* field to show
|
||||
average order prices over time.
|
||||
|
||||
To break down your data, drag the *category.keyword* field to the visualization builder pane. Lens
|
||||
understands that you want to show the top categories and compare them across the dates,
|
||||
and creates a chart that compares the sales for each of the top 3 categories:
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/lens_tutorial_2.png[Lens tutorial]
|
||||
|
||||
[float]
|
||||
[[customize-lens-visualization]]
|
||||
==== Further customization
|
||||
|
||||
Customize your visualization to look exactly how you want.
|
||||
|
||||
. In the right column, click *Average of taxful_total_price*.
|
||||
|
||||
.. Change the *Label* to `Sales`, or a name that you prefer for the data.
|
||||
|
||||
. Click *Top values of category.keyword*.
|
||||
|
||||
.. Increase *Number of values* to `10`. The visualization updates in the background to show there are only
|
||||
six available categories.
|
||||
|
||||
. Look at the suggestions. None of them show an area chart, but for sales data, a stacked area chart
|
||||
might make sense. To switch the chart type:
|
||||
|
||||
.. Click *Stacked bar chart* in the right column.
|
||||
|
||||
.. Click *Stacked area*.
|
||||
+
|
||||
[role="screenshot"]
|
||||
image::images/lens_tutorial_3.png[Lens tutorial]
|
||||
|
||||
[float]
|
||||
[[lens-tutorial-next-steps]]
|
||||
==== Next steps
|
||||
|
||||
Now that you've created your visualization in Lens, you can add it to a Dashboard.
|
||||
|
||||
For more information, see <<dashboard,Dashboard>>.
|
|
@ -330,6 +330,7 @@
|
|||
"@types/pngjs": "^3.3.2",
|
||||
"@types/podium": "^1.0.0",
|
||||
"@types/prop-types": "^15.5.3",
|
||||
"@types/reach__router": "^1.2.6",
|
||||
"@types/react": "^16.8.0",
|
||||
"@types/react-dom": "^16.8.0",
|
||||
"@types/react-redux": "^6.0.6",
|
||||
|
@ -349,8 +350,8 @@
|
|||
"@types/uuid": "^3.4.4",
|
||||
"@types/vinyl-fs": "^2.4.11",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.7.0",
|
||||
"@typescript-eslint/parser": "^2.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.8.0",
|
||||
"@typescript-eslint/parser": "^2.8.0",
|
||||
"angular-mocks": "^1.7.8",
|
||||
"archiver": "^3.1.1",
|
||||
"axe-core": "^3.3.2",
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
},
|
||||
"homepage": "https://github.com/elastic/eslint-config-kibana#readme",
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^2.7.0",
|
||||
"@typescript-eslint/parser": "^2.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.8.0",
|
||||
"@typescript-eslint/parser": "^2.8.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import _ from 'lodash';
|
||||
import { migrateFilter } from '../migrate_filter';
|
||||
|
||||
describe('migrateFilter', function () {
|
||||
|
||||
const oldMatchPhraseFilter = {
|
||||
match: {
|
||||
fieldFoo: {
|
||||
query: 'foobar',
|
||||
type: 'phrase'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const newMatchPhraseFilter = {
|
||||
match_phrase: {
|
||||
fieldFoo: {
|
||||
query: 'foobar'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// https://github.com/elastic/elasticsearch/pull/17508
|
||||
it('should migrate match filters of type phrase', function () {
|
||||
const migratedFilter = migrateFilter(oldMatchPhraseFilter);
|
||||
expect(_.isEqual(migratedFilter, newMatchPhraseFilter)).to.be(true);
|
||||
});
|
||||
|
||||
it('should not modify the original filter', function () {
|
||||
const oldMatchPhraseFilterCopy = _.clone(oldMatchPhraseFilter, true);
|
||||
migrateFilter(oldMatchPhraseFilter);
|
||||
expect(_.isEqual(oldMatchPhraseFilter, oldMatchPhraseFilterCopy)).to.be(true);
|
||||
});
|
||||
|
||||
it('should return the original filter if no migration is necessary', function () {
|
||||
const originalFilter = {
|
||||
match_all: {}
|
||||
};
|
||||
const migratedFilter = migrateFilter(originalFilter);
|
||||
expect(migratedFilter).to.be(originalFilter);
|
||||
expect(_.isEqual(migratedFilter, originalFilter)).to.be(true);
|
||||
});
|
||||
|
||||
});
|
|
@ -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 expect from '@kbn/expect';
|
||||
import { buildQueryFromFilters } from '../from_filters';
|
||||
|
||||
describe('build query', function () {
|
||||
describe('buildQueryFromFilters', function () {
|
||||
it('should return the parameters of an Elasticsearch bool query', function () {
|
||||
const result = buildQueryFromFilters([]);
|
||||
const expected = {
|
||||
must: [],
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
};
|
||||
expect(result).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should transform an array of kibana filters into ES queries combined in the bool clauses', function () {
|
||||
const filters = [
|
||||
{
|
||||
match_all: {},
|
||||
meta: { type: 'match_all' },
|
||||
},
|
||||
{
|
||||
exists: { field: 'foo' },
|
||||
meta: { type: 'exists' },
|
||||
},
|
||||
];
|
||||
|
||||
const expectedESQueries = [
|
||||
{ match_all: {} },
|
||||
{ exists: { field: 'foo' } },
|
||||
];
|
||||
|
||||
const result = buildQueryFromFilters(filters);
|
||||
|
||||
expect(result.filter).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
it('should remove disabled filters', function () {
|
||||
const filters = [
|
||||
{
|
||||
match_all: {},
|
||||
meta: { type: 'match_all', negate: true, disabled: true },
|
||||
},
|
||||
];
|
||||
|
||||
const expectedESQueries = [];
|
||||
|
||||
const result = buildQueryFromFilters(filters);
|
||||
|
||||
expect(result.must_not).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
it('should remove falsy filters', function () {
|
||||
const filters = [null, undefined];
|
||||
|
||||
const expectedESQueries = [];
|
||||
|
||||
const result = buildQueryFromFilters(filters);
|
||||
|
||||
expect(result.must_not).to.eql(expectedESQueries);
|
||||
expect(result.must).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
it('should place negated filters in the must_not clause', function () {
|
||||
const filters = [
|
||||
{
|
||||
match_all: {},
|
||||
meta: { type: 'match_all', negate: true },
|
||||
},
|
||||
];
|
||||
|
||||
const expectedESQueries = [{ match_all: {} }];
|
||||
|
||||
const result = buildQueryFromFilters(filters);
|
||||
|
||||
expect(result.must_not).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
it('should translate old ES filter syntax into ES 5+ query objects', function () {
|
||||
const filters = [
|
||||
{
|
||||
query: { exists: { field: 'foo' } },
|
||||
meta: { type: 'exists' },
|
||||
},
|
||||
];
|
||||
|
||||
const expectedESQueries = [
|
||||
{
|
||||
exists: { field: 'foo' },
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildQueryFromFilters(filters);
|
||||
|
||||
expect(result.filter).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
it('should migrate deprecated match syntax', function () {
|
||||
const filters = [
|
||||
{
|
||||
query: { match: { extension: { query: 'foo', type: 'phrase' } } },
|
||||
meta: { type: 'phrase' },
|
||||
},
|
||||
];
|
||||
|
||||
const expectedESQueries = [
|
||||
{
|
||||
match_phrase: { extension: { query: 'foo' } },
|
||||
},
|
||||
];
|
||||
|
||||
const result = buildQueryFromFilters(filters);
|
||||
|
||||
expect(result.filter).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
it('should not add query:queryString:options to query_string filters', function () {
|
||||
const filters = [
|
||||
{
|
||||
query: { query_string: { query: 'foo' } },
|
||||
meta: { type: 'query_string' },
|
||||
},
|
||||
];
|
||||
const expectedESQueries = [{ query_string: { query: 'foo' } }];
|
||||
|
||||
const result = buildQueryFromFilters(filters);
|
||||
|
||||
expect(result.filter).to.eql(expectedESQueries);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 expect from '@kbn/expect';
|
||||
import { buildQueryFromLucene } from '../from_lucene';
|
||||
import { decorateQuery } from '../decorate_query';
|
||||
import { luceneStringToDsl } from '../lucene_string_to_dsl';
|
||||
|
||||
describe('build query', function () {
|
||||
|
||||
describe('buildQueryFromLucene', function () {
|
||||
|
||||
it('should return the parameters of an Elasticsearch bool query', function () {
|
||||
const result = buildQueryFromLucene();
|
||||
const expected = {
|
||||
must: [],
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
};
|
||||
expect(result).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should transform an array of lucene queries into ES queries combined in the bool\'s must clause', function () {
|
||||
const queries = [
|
||||
{ query: 'foo:bar', language: 'lucene' },
|
||||
{ query: 'bar:baz', language: 'lucene' },
|
||||
];
|
||||
|
||||
const expectedESQueries = queries.map(
|
||||
(query) => {
|
||||
return decorateQuery(luceneStringToDsl(query.query), {});
|
||||
}
|
||||
);
|
||||
|
||||
const result = buildQueryFromLucene(queries, {});
|
||||
|
||||
expect(result.must).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
it('should also accept queries in ES query DSL format, simply passing them through', function () {
|
||||
const queries = [
|
||||
{ query: { match_all: {} }, language: 'lucene' },
|
||||
];
|
||||
|
||||
const result = buildQueryFromLucene(queries, {});
|
||||
|
||||
expect(result.must).to.eql([queries[0].query]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should accept a date format in the decorated queries and combine that into the bool\'s must clause', function () {
|
||||
const queries = [
|
||||
{ query: 'foo:bar', language: 'lucene' },
|
||||
{ query: 'bar:baz', language: 'lucene' },
|
||||
];
|
||||
const dateFormatTZ = 'America/Phoenix';
|
||||
|
||||
const expectedESQueries = queries.map(
|
||||
(query) => {
|
||||
return decorateQuery(luceneStringToDsl(query.query), {}, dateFormatTZ);
|
||||
}
|
||||
);
|
||||
|
||||
const result = buildQueryFromLucene(queries, {}, dateFormatTZ);
|
||||
|
||||
expect(result.must).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
});
|
1
packages/kbn-es-query/src/index.d.ts
vendored
|
@ -17,5 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './es_query';
|
||||
export * from './kuery';
|
||||
|
|
|
@ -18,4 +18,3 @@
|
|||
*/
|
||||
|
||||
export * from './kuery';
|
||||
export * from './es_query';
|
||||
|
|
14
packages/kbn-es-query/src/kuery/ast/ast.d.ts
vendored
|
@ -25,18 +25,26 @@ import { JsonObject } from '..';
|
|||
|
||||
export type KueryNode = any;
|
||||
|
||||
export type DslQuery = any;
|
||||
|
||||
export interface KueryParseOptions {
|
||||
helpers: {
|
||||
[key: string]: any;
|
||||
};
|
||||
startRule: string;
|
||||
allowLeadingWildcards: boolean;
|
||||
}
|
||||
|
||||
export function fromKueryExpression(
|
||||
expression: string,
|
||||
parseOptions?: KueryParseOptions
|
||||
expression: string | DslQuery,
|
||||
parseOptions?: Partial<KueryParseOptions>
|
||||
): KueryNode;
|
||||
|
||||
export function toElasticsearchQuery(node: KueryNode, indexPattern?: any): JsonObject;
|
||||
export function toElasticsearchQuery(
|
||||
node: KueryNode,
|
||||
indexPattern?: any,
|
||||
config?: Record<string, any>,
|
||||
context?: Record<string, any>
|
||||
): JsonObject;
|
||||
|
||||
export function doesKueryExpressionHaveLuceneSyntaxError(expression: string): boolean;
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { convertExistsFilter } from '../exists';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('exists filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'exists',
|
||||
key: 'foo',
|
||||
}
|
||||
};
|
||||
const result = convertExistsFilter(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'exists');
|
||||
expect(result.arguments[0].value).to.be('foo');
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "exists"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertExistsFilter).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "exists", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import expect from '@kbn/expect';
|
||||
import { filterToKueryAST } from '../filter_to_kuery';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('filterToKueryAST', function () {
|
||||
|
||||
it('should hand off conversion of known filter types to the appropriate converter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'exists',
|
||||
key: 'foo',
|
||||
}
|
||||
};
|
||||
const result = filterToKueryAST(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'exists');
|
||||
});
|
||||
|
||||
it('should thrown an error when an unknown filter type is encountered', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo',
|
||||
}
|
||||
};
|
||||
|
||||
expect(filterToKueryAST).withArgs(filter).to.throwException(/Couldn't convert that filter to a kuery/);
|
||||
});
|
||||
|
||||
it('should wrap the AST node of negated filters in a "not" function', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'exists',
|
||||
key: 'foo',
|
||||
}
|
||||
};
|
||||
const negatedFilter = _.set(_.cloneDeep(filter), 'meta.negate', true);
|
||||
|
||||
const result = filterToKueryAST(filter);
|
||||
const negatedResult = filterToKueryAST(negatedFilter);
|
||||
|
||||
expect(negatedResult).to.have.property('type', 'function');
|
||||
expect(negatedResult).to.have.property('function', 'not');
|
||||
expect(negatedResult.arguments[0]).to.eql(result);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import expect from '@kbn/expect';
|
||||
import { convertGeoBoundingBox } from '../geo_bounding_box';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('geo_bounding_box filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'geo_bounding_box',
|
||||
key: 'foo',
|
||||
params: {
|
||||
topLeft: {
|
||||
lat: 10,
|
||||
lon: 20,
|
||||
},
|
||||
bottomRight: {
|
||||
lat: 30,
|
||||
lon: 40,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
const result = convertGeoBoundingBox(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'geoBoundingBox');
|
||||
|
||||
const { arguments: [ { value: fieldName }, ...args ] } = result;
|
||||
expect(fieldName).to.be('foo');
|
||||
|
||||
const argByName = _.mapKeys(args, 'name');
|
||||
expect(argByName.topLeft.value.value).to.be('10, 20');
|
||||
expect(argByName.bottomRight.value.value).to.be('30, 40');
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "geo_bounding_box"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertGeoBoundingBox).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "geo_bounding_box", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES 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 { convertGeoPolygon } from '../geo_polygon';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('geo_polygon filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'geo_polygon',
|
||||
key: 'foo',
|
||||
params: {
|
||||
points: [
|
||||
{
|
||||
lat: 10,
|
||||
lon: 20,
|
||||
},
|
||||
{
|
||||
lat: 30,
|
||||
lon: 40,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = convertGeoPolygon(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'geoPolygon');
|
||||
|
||||
const { arguments: [ { value: fieldName }, ...args ] } = result;
|
||||
expect(fieldName).to.be('foo');
|
||||
|
||||
expect(args[0].value).to.be('10, 20');
|
||||
expect(args[1].value).to.be('30, 40');
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "geo_polygon"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertGeoPolygon).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "geo_polygon", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { convertPhraseFilter } from '../phrase';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('phrase filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'phrase',
|
||||
key: 'foo',
|
||||
params: {
|
||||
query: 'bar'
|
||||
},
|
||||
}
|
||||
};
|
||||
const result = convertPhraseFilter(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'is');
|
||||
|
||||
const { arguments: [ { value: fieldName }, { value: value } ] } = result;
|
||||
expect(fieldName).to.be('foo');
|
||||
expect(value).to.be('bar');
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "phrase"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertPhraseFilter).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "phrase", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import expect from '@kbn/expect';
|
||||
import { convertRangeFilter } from '../range';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('range filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'range',
|
||||
key: 'foo',
|
||||
params: {
|
||||
gt: 1000,
|
||||
lt: 8000,
|
||||
},
|
||||
}
|
||||
};
|
||||
const result = convertRangeFilter(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'range');
|
||||
|
||||
const { arguments: [ { value: fieldName }, ...args ] } = result;
|
||||
expect(fieldName).to.be('foo');
|
||||
|
||||
const argByName = _.mapKeys(args, 'name');
|
||||
expect(argByName.gt.value.value).to.be(1000);
|
||||
expect(argByName.lt.value.value).to.be(8000);
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "range"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertRangeFilter).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "range", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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 { nodeTypes } from '../node_types';
|
||||
import { convertPhraseFilter } from './phrase';
|
||||
import { convertRangeFilter } from './range';
|
||||
import { convertExistsFilter } from './exists';
|
||||
import { convertGeoBoundingBox } from './geo_bounding_box';
|
||||
import { convertGeoPolygon } from './geo_polygon';
|
||||
|
||||
const conversionChain = [
|
||||
convertPhraseFilter,
|
||||
convertRangeFilter,
|
||||
convertExistsFilter,
|
||||
convertGeoBoundingBox,
|
||||
convertGeoPolygon,
|
||||
];
|
||||
|
||||
export function filterToKueryAST(filter) {
|
||||
const { negate } = filter.meta;
|
||||
|
||||
const node = conversionChain.reduce((acc, converter) => {
|
||||
if (acc !== null) return acc;
|
||||
|
||||
try {
|
||||
return converter(filter);
|
||||
}
|
||||
catch (ex) {
|
||||
return null;
|
||||
}
|
||||
}, null);
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`Couldn't convert that filter to a kuery`);
|
||||
}
|
||||
|
||||
return negate ? nodeTypes.function.buildNode('not', node) : node;
|
||||
}
|
|
@ -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 { nodeTypes } from '../node_types';
|
||||
|
||||
export function convertRangeFilter(filter) {
|
||||
if (filter.meta.type !== 'range') {
|
||||
throw new Error(`Expected filter of type "range", got "${filter.meta.type}"`);
|
||||
}
|
||||
|
||||
const { key, params } = filter.meta;
|
||||
return nodeTypes.function.buildNode('range', key, params);
|
||||
}
|
|
@ -18,6 +18,5 @@
|
|||
*/
|
||||
|
||||
export * from './ast';
|
||||
export * from './filter_migration';
|
||||
export { nodeTypes } from './node_types';
|
||||
export * from './errors';
|
||||
|
|
|
@ -24,10 +24,9 @@ function isVersionFlag(a) {
|
|||
}
|
||||
|
||||
function getCustomSnapshotUrl() {
|
||||
// force use of manually created snapshots until live ones are available
|
||||
// force use of manually created snapshots until ReindexPutMappings fix
|
||||
if (!process.env.KBN_ES_SNAPSHOT_URL && !process.argv.some(isVersionFlag)) {
|
||||
// return 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}-{os}-x86_64.{ext}';
|
||||
return;
|
||||
return 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}-{os}-x86_64.{ext}';
|
||||
}
|
||||
|
||||
if (process.env.KBN_ES_SNAPSHOT_URL && process.env.KBN_ES_SNAPSHOT_URL !== 'false') {
|
||||
|
|
|
@ -58,7 +58,7 @@ export class Config {
|
|||
this[$values] = value;
|
||||
}
|
||||
|
||||
public has(key: string) {
|
||||
public has(key: string | string[]) {
|
||||
function recursiveHasCheck(
|
||||
remainingPath: string[],
|
||||
values: Record<string, any>,
|
||||
|
@ -109,7 +109,7 @@ export class Config {
|
|||
return recursiveHasCheck(path, this[$values], schema);
|
||||
}
|
||||
|
||||
public get(key: string, defaultValue?: any) {
|
||||
public get(key: string | string[], defaultValue?: any) {
|
||||
if (!this.has(key)) {
|
||||
throw new Error(`Unknown config key "${key}"`);
|
||||
}
|
||||
|
|
|
@ -529,6 +529,14 @@
|
|||
'@types/podium',
|
||||
],
|
||||
},
|
||||
{
|
||||
groupSlug: '@reach/router',
|
||||
groupName: '@reach/router related packages',
|
||||
packageNames: [
|
||||
'@reach/router',
|
||||
'@types/reach__router',
|
||||
],
|
||||
},
|
||||
{
|
||||
groupSlug: 'request',
|
||||
groupName: 'request related packages',
|
||||
|
|
|
@ -1130,7 +1130,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy';
|
|||
| ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `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 { QueryBarInput } from '../data/public'` | Directives are deprecated. |
|
||||
| `import 'ui/query_bar'` | `import { 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 '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. |
|
||||
| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | |
|
||||
|
@ -1142,6 +1142,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy';
|
|||
| `ui/registry/feature_catalogue | `feature_catalogue.register` | Must add `feature_catalogue` as a dependency in your kibana.json. |
|
||||
| `ui/registry/vis_types` | `visualizations.types` | -- |
|
||||
| `ui/vis` | `visualizations.types` | -- |
|
||||
| `ui/share` | `share` | `showShareContextMenu` is now called `toggleShareContextMenu`, `ShareContextMenuExtensionsRegistryProvider` is now called `register` |
|
||||
| `ui/vis/vis_factory` | `visualizations.types` | -- |
|
||||
| `ui/vis/vis_filters` | `visualizations.filters` | -- |
|
||||
| `ui/utils/parse_es_interval` | `import { parseEsInterval } from '../data/public'` | `parseEsInterval`, `ParsedInterval`, `InvalidEsCalendarIntervalError`, `InvalidEsIntervalFormatError` items were moved to the `Data Plugin` as a static code |
|
||||
|
|
|
@ -20,11 +20,12 @@
|
|||
import { sortBy } from 'lodash';
|
||||
import { BehaviorSubject, ReplaySubject, Observable } from 'rxjs';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { MountPoint } from '../../types';
|
||||
|
||||
/** @public */
|
||||
export interface ChromeNavControl {
|
||||
order?: number;
|
||||
mount(targetDomElement: HTMLElement): () => void;
|
||||
mount: MountPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MountPoint } from '../../../types';
|
||||
|
||||
interface Props {
|
||||
extension?: (el: HTMLDivElement) => () => void;
|
||||
extension?: MountPoint<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export class HeaderExtension extends React.Component<Props> {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import angular from 'angular';
|
||||
import { InternalCoreSetup, InternalCoreStart } from '../core_system';
|
||||
import { LegacyCoreSetup, LegacyCoreStart } from '../';
|
||||
import { LegacyCoreSetup, LegacyCoreStart, MountPoint } from '../';
|
||||
|
||||
/** @internal */
|
||||
export interface LegacyPlatformParams {
|
||||
|
@ -40,7 +40,7 @@ interface StartDeps {
|
|||
}
|
||||
|
||||
interface BootstrapModule {
|
||||
bootstrap: (targetDomElement: HTMLElement) => void;
|
||||
bootstrap: MountPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -117,9 +117,22 @@ function createCoreContext(): CoreContext {
|
|||
};
|
||||
}
|
||||
|
||||
function createStorageMock() {
|
||||
const storageMock: jest.Mocked<Storage> = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
key: jest.fn(),
|
||||
length: 10,
|
||||
};
|
||||
return storageMock;
|
||||
}
|
||||
|
||||
export const coreMock = {
|
||||
createCoreContext,
|
||||
createSetup: createCoreSetupMock,
|
||||
createStart: createCoreStartMock,
|
||||
createPluginInitializerContext: pluginInitializerContextMock,
|
||||
createStorage: createStorageMock,
|
||||
};
|
||||
|
|
|
@ -40,6 +40,7 @@ function render(props: ErrorToastProps = {}) {
|
|||
error={props.error || new Error('error message')}
|
||||
title={props.title || 'An error occured'}
|
||||
toastMessage={props.toastMessage || 'This is the toast message'}
|
||||
i18nContext={() => ({ children }) => <React.Fragment>{children}</React.Fragment>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
|
@ -32,12 +33,14 @@ import { EuiSpacer } from '@elastic/eui';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { OverlayStart } from '../../overlays';
|
||||
import { I18nStart } from '../../i18n';
|
||||
|
||||
interface ErrorToastProps {
|
||||
title: string;
|
||||
error: Error;
|
||||
toastMessage: string;
|
||||
openModal: OverlayStart['openModal'];
|
||||
i18nContext: () => I18nStart['Context'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,33 +53,48 @@ function showErrorDialog({
|
|||
title,
|
||||
error,
|
||||
openModal,
|
||||
}: Pick<ErrorToastProps, 'error' | 'title' | 'openModal'>) {
|
||||
i18nContext,
|
||||
}: Pick<ErrorToastProps, 'error' | 'title' | 'openModal' | 'i18nContext'>) {
|
||||
const I18nContext = i18nContext();
|
||||
const modal = openModal(
|
||||
<React.Fragment>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiCallOut size="s" color="danger" iconType="alert" title={error.message} />
|
||||
{error.stack && (
|
||||
<React.Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCodeBlock isCopyable={true} paddingSize="s">
|
||||
{error.stack}
|
||||
</EuiCodeBlock>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton onClick={() => modal.close()} fill>
|
||||
<FormattedMessage id="core.notifications.errorToast.closeModal" defaultMessage="Close" />
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</React.Fragment>
|
||||
mount(
|
||||
<React.Fragment>
|
||||
<I18nContext>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiCallOut size="s" color="danger" iconType="alert" title={error.message} />
|
||||
{error.stack && (
|
||||
<React.Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCodeBlock isCopyable={true} paddingSize="s">
|
||||
{error.stack}
|
||||
</EuiCodeBlock>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton onClick={() => modal.close()} fill>
|
||||
<FormattedMessage
|
||||
id="core.notifications.errorToast.closeModal"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</I18nContext>
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorToast({ title, error, toastMessage, openModal }: ErrorToastProps) {
|
||||
export function ErrorToast({
|
||||
title,
|
||||
error,
|
||||
toastMessage,
|
||||
openModal,
|
||||
i18nContext,
|
||||
}: ErrorToastProps) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p data-test-subj="errorToastMessage">{toastMessage}</p>
|
||||
|
@ -84,7 +102,7 @@ export function ErrorToast({ title, error, toastMessage, openModal }: ErrorToast
|
|||
<EuiButton
|
||||
size="s"
|
||||
color="danger"
|
||||
onClick={() => showErrorDialog({ title, error, openModal })}
|
||||
onClick={() => showErrorDialog({ title, error, openModal, i18nContext })}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="core.toasts.errorToast.seeFullError"
|
||||
|
@ -95,3 +113,8 @@ export function ErrorToast({ title, error, toastMessage, openModal }: ErrorToast
|
|||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const mount = (component: React.ReactElement) => (container: HTMLElement) => {
|
||||
ReactDOM.render(component, container);
|
||||
return () => ReactDOM.unmountComponentAtNode(container);
|
||||
};
|
||||
|
|
|
@ -51,10 +51,13 @@ function uiSettingsMock() {
|
|||
function toastDeps() {
|
||||
return {
|
||||
uiSettings: uiSettingsMock(),
|
||||
i18n: i18nServiceMock.createStartContract(),
|
||||
};
|
||||
}
|
||||
|
||||
function startDeps() {
|
||||
return { overlays: {} as any, i18n: i18nServiceMock.createStartContract() };
|
||||
}
|
||||
|
||||
describe('#get$()', () => {
|
||||
it('returns observable that emits NEW toast list when something added or removed', () => {
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
|
@ -188,6 +191,7 @@ describe('#addDanger()', () => {
|
|||
describe('#addError', () => {
|
||||
it('adds an error toast', async () => {
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
toasts.start(startDeps());
|
||||
const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' });
|
||||
expect(toast).toHaveProperty('color', 'danger');
|
||||
expect(toast).toHaveProperty('title', 'Something went wrong');
|
||||
|
@ -195,6 +199,7 @@ describe('#addError', () => {
|
|||
|
||||
it('returns the created toast', async () => {
|
||||
const toasts = new ToastsApi(toastDeps());
|
||||
toasts.start(startDeps());
|
||||
const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' });
|
||||
const currentToasts = await getCurrentToasts(toasts);
|
||||
expect(currentToasts[0]).toBe(toast);
|
||||
|
|
|
@ -26,6 +26,7 @@ import { MountPoint } from '../../types';
|
|||
import { mountReactNode } from '../../utils';
|
||||
import { UiSettingsClientContract } from '../../ui_settings';
|
||||
import { OverlayStart } from '../../overlays';
|
||||
import { I18nStart } from '../../i18n';
|
||||
|
||||
/**
|
||||
* Allowed fields for {@link ToastInput}.
|
||||
|
@ -96,14 +97,16 @@ export class ToastsApi implements IToasts {
|
|||
private uiSettings: UiSettingsClientContract;
|
||||
|
||||
private overlays?: OverlayStart;
|
||||
private i18n?: I18nStart;
|
||||
|
||||
constructor(deps: { uiSettings: UiSettingsClientContract }) {
|
||||
this.uiSettings = deps.uiSettings;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public registerOverlays(overlays: OverlayStart) {
|
||||
public start({ overlays, i18n }: { overlays: OverlayStart; i18n: I18nStart }) {
|
||||
this.overlays = overlays;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/** Observable of the toast messages to show to the user. */
|
||||
|
@ -206,6 +209,7 @@ export class ToastsApi implements IToasts {
|
|||
error={error}
|
||||
title={options.title}
|
||||
toastMessage={message}
|
||||
i18nContext={() => this.i18n!.Context}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
|
|
@ -58,7 +58,7 @@ export class ToastsService {
|
|||
}
|
||||
|
||||
public start({ i18n, overlays, targetDomElement }: StartDeps) {
|
||||
this.api!.registerOverlays(overlays);
|
||||
this.api!.start({ overlays, i18n });
|
||||
this.targetDomElement = targetDomElement;
|
||||
|
||||
render(
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FlyoutService FlyoutRef#close() can be called multiple times on the same FlyoutRef 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`FlyoutService openFlyout() renders a flyout to the DOM 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<mockConstructor>
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
hideCloseButton={false}
|
||||
maxWidth={false}
|
||||
onClose={[Function]}
|
||||
ownFocus={false}
|
||||
size="m"
|
||||
>
|
||||
<span>
|
||||
Flyout content
|
||||
</span>
|
||||
</EuiFlyout>
|
||||
</mockConstructor>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<mockConstructor>
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
hideCloseButton={false}
|
||||
maxWidth={false}
|
||||
onClose={[Function]}
|
||||
ownFocus={false}
|
||||
size="m"
|
||||
>
|
||||
<span>
|
||||
Flyout content 1
|
||||
</span>
|
||||
</EuiFlyout>
|
||||
</mockConstructor>,
|
||||
<div />,
|
||||
],
|
||||
Array [
|
||||
<mockConstructor>
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
hideCloseButton={false}
|
||||
maxWidth={false}
|
||||
onClose={[Function]}
|
||||
ownFocus={false}
|
||||
size="m"
|
||||
>
|
||||
<span>
|
||||
Flyout content 2
|
||||
</span>
|
||||
</EuiFlyout>
|
||||
</mockConstructor>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
|
@ -1,64 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ModalService ModalRef#close() can be called multiple times on the same ModalRef 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openModal() renders a modal to the DOM 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
>
|
||||
<span>
|
||||
Modal content
|
||||
</span>
|
||||
</EuiModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
>
|
||||
<span>
|
||||
Modal content 1
|
||||
</span>
|
||||
</EuiModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
>
|
||||
<span>
|
||||
Flyout content 2
|
||||
</span>
|
||||
</EuiModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
|
@ -1 +1,2 @@
|
|||
@import './banners/index';
|
||||
@import './mount_wrapper';
|
||||
|
|
5
src/core/public/overlays/_mount_wrapper.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
.kbnOverlayMountWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
|
@ -97,9 +97,7 @@ export class OverlayBannersService {
|
|||
if (!banners$.value.has(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
banners$.next(banners$.value.remove(id));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
|
@ -107,10 +105,8 @@ export class OverlayBannersService {
|
|||
if (!id || !banners$.value.has(id)) {
|
||||
return this.add(mount, priority);
|
||||
}
|
||||
|
||||
const nextId = genId();
|
||||
const nextBanner = { id: nextId, mount, priority };
|
||||
|
||||
banners$.next(banners$.value.remove(id).add(nextId, nextBanner));
|
||||
return nextId;
|
||||
},
|
||||
|
|
77
src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FlyoutService FlyoutRef#close() can be called multiple times on the same FlyoutRef 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`FlyoutService openFlyout() renders a flyout to the DOM 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<mockConstructor>
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
hideCloseButton={false}
|
||||
maxWidth={false}
|
||||
onClose={[Function]}
|
||||
ownFocus={false}
|
||||
size="m"
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</mockConstructor>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"<span><div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"false\\"><div role=\\"dialog\\" class=\\"euiFlyout euiFlyout--medium\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiFlyout__closeButton\\" type=\\"button\\" aria-label=\\"Closes this dialog\\" data-test-subj=\\"euiFlyoutCloseButton\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"></svg></button><div class=\\"kbnOverlayMountWrapper\\"><span>Flyout content</span></div></div></div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div></div></span>"`;
|
||||
|
||||
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<mockConstructor>
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
hideCloseButton={false}
|
||||
maxWidth={false}
|
||||
onClose={[Function]}
|
||||
ownFocus={false}
|
||||
size="m"
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</mockConstructor>,
|
||||
<div />,
|
||||
],
|
||||
Array [
|
||||
<mockConstructor>
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
hideCloseButton={false}
|
||||
maxWidth={false}
|
||||
onClose={[Function]}
|
||||
ownFocus={false}
|
||||
size="m"
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiFlyout>
|
||||
</mockConstructor>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"<span><div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"false\\"><div role=\\"dialog\\" class=\\"euiFlyout euiFlyout--medium\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiFlyout__closeButton\\" type=\\"button\\" aria-label=\\"Closes this dialog\\" data-test-subj=\\"euiFlyoutCloseButton\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"></svg></button><div class=\\"kbnOverlayMountWrapper\\"><span>Flyout content 2</span></div></div></div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div></div></span>"`;
|
|
@ -17,23 +17,27 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export function buildQueryFromFilters(filters: unknown[], indexPattern: unknown): unknown;
|
||||
export function buildEsQuery(
|
||||
indexPattern: unknown,
|
||||
queries: unknown,
|
||||
filters: unknown,
|
||||
config?: {
|
||||
allowLeadingWildcards: boolean;
|
||||
queryStringOptions: unknown;
|
||||
ignoreFilterIfFieldNotInIndex: boolean;
|
||||
dateFormatTZ?: string | null;
|
||||
}
|
||||
): unknown;
|
||||
export function getEsQueryConfig(config: {
|
||||
get: (name: string) => unknown;
|
||||
}): {
|
||||
allowLeadingWildcards: boolean;
|
||||
queryStringOptions: unknown;
|
||||
ignoreFilterIfFieldNotInIndex: boolean;
|
||||
dateFormatTZ?: string | null;
|
||||
import { FlyoutService, OverlayFlyoutStart } from './flyout_service';
|
||||
|
||||
const createStartContractMock = () => {
|
||||
const startContract: jest.Mocked<OverlayFlyoutStart> = {
|
||||
open: jest.fn().mockReturnValue({
|
||||
close: jest.fn(),
|
||||
onClose: Promise.resolve(),
|
||||
}),
|
||||
};
|
||||
return startContract;
|
||||
};
|
||||
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<PublicMethodsOf<FlyoutService>> = {
|
||||
start: jest.fn(),
|
||||
};
|
||||
mocked.start.mockReturnValue(createStartContractMock());
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const overlayFlyoutServiceMock = {
|
||||
create: createMock,
|
||||
createStartContract: createStartContractMock,
|
||||
};
|
|
@ -16,11 +16,12 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { mockReactDomRender, mockReactDomUnmount } from './flyout.test.mocks';
|
||||
import { mockReactDomRender, mockReactDomUnmount } from '../overlay.test.mocks';
|
||||
|
||||
import React from 'react';
|
||||
import { i18nServiceMock } from '../i18n/i18n_service.mock';
|
||||
import { FlyoutRef, FlyoutService } from './flyout';
|
||||
import { mount } from 'enzyme';
|
||||
import { i18nServiceMock } from '../../i18n/i18n_service.mock';
|
||||
import { FlyoutService, OverlayFlyoutStart } from './flyout_service';
|
||||
import { OverlayRef } from '../types';
|
||||
|
||||
const i18nMock = i18nServiceMock.createStartContract();
|
||||
|
||||
|
@ -29,35 +30,50 @@ beforeEach(() => {
|
|||
mockReactDomUnmount.mockClear();
|
||||
});
|
||||
|
||||
const mountText = (text: string) => (container: HTMLElement) => {
|
||||
const content = document.createElement('span');
|
||||
content.textContent = text;
|
||||
container.append(content);
|
||||
return () => {};
|
||||
};
|
||||
|
||||
const getServiceStart = () => {
|
||||
const service = new FlyoutService();
|
||||
return service.start({ i18n: i18nMock, targetDomElement: document.createElement('div') });
|
||||
};
|
||||
|
||||
describe('FlyoutService', () => {
|
||||
let flyouts: OverlayFlyoutStart;
|
||||
beforeEach(() => {
|
||||
flyouts = getServiceStart();
|
||||
});
|
||||
|
||||
describe('openFlyout()', () => {
|
||||
it('renders a flyout to the DOM', () => {
|
||||
const target = document.createElement('div');
|
||||
const flyoutService = new FlyoutService(target);
|
||||
expect(mockReactDomRender).not.toHaveBeenCalled();
|
||||
flyoutService.openFlyout(i18nMock, <span>Flyout content</span>);
|
||||
flyouts.open(mountText('Flyout content'));
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
const modalContent = mount(mockReactDomRender.mock.calls[0][0]);
|
||||
expect(modalContent.html()).toMatchSnapshot();
|
||||
});
|
||||
describe('with a currently active flyout', () => {
|
||||
let target: HTMLElement;
|
||||
let flyoutService: FlyoutService;
|
||||
let ref1: FlyoutRef;
|
||||
let ref1: OverlayRef;
|
||||
beforeEach(() => {
|
||||
target = document.createElement('div');
|
||||
flyoutService = new FlyoutService(target);
|
||||
ref1 = flyoutService.openFlyout(i18nMock, <span>Flyout content 1</span>);
|
||||
ref1 = flyouts.open(mountText('Flyout content'));
|
||||
});
|
||||
it('replaces the current flyout with a new one', () => {
|
||||
flyoutService.openFlyout(i18nMock, <span>Flyout content 2</span>);
|
||||
flyouts.open(mountText('Flyout content 2'));
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
const modalContent = mount(mockReactDomRender.mock.calls[1][0]);
|
||||
expect(modalContent.html()).toMatchSnapshot();
|
||||
expect(() => ref1.close()).not.toThrowError();
|
||||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('resolves onClose on the previous ref', async () => {
|
||||
const onCloseComplete = jest.fn();
|
||||
ref1.onClose.then(onCloseComplete);
|
||||
flyoutService.openFlyout(i18nMock, <span>Flyout content 2</span>);
|
||||
flyouts.open(mountText('Flyout content 2'));
|
||||
await ref1.onClose;
|
||||
expect(onCloseComplete).toBeCalledTimes(1);
|
||||
});
|
||||
|
@ -65,9 +81,7 @@ describe('FlyoutService', () => {
|
|||
});
|
||||
describe('FlyoutRef#close()', () => {
|
||||
it('resolves the onClose Promise', async () => {
|
||||
const target = document.createElement('div');
|
||||
const flyoutService = new FlyoutService(target);
|
||||
const ref = flyoutService.openFlyout(i18nMock, <span>Flyout content</span>);
|
||||
const ref = flyouts.open(mountText('Flyout content'));
|
||||
|
||||
const onCloseComplete = jest.fn();
|
||||
ref.onClose.then(onCloseComplete);
|
||||
|
@ -76,9 +90,7 @@ describe('FlyoutService', () => {
|
|||
expect(onCloseComplete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('can be called multiple times on the same FlyoutRef', async () => {
|
||||
const target = document.createElement('div');
|
||||
const flyoutService = new FlyoutService(target);
|
||||
const ref = flyoutService.openFlyout(i18nMock, <span>Flyout content</span>);
|
||||
const ref = flyouts.open(mountText('Flyout content'));
|
||||
expect(mockReactDomUnmount).not.toHaveBeenCalled();
|
||||
await ref.close();
|
||||
expect(mockReactDomUnmount.mock.calls).toMatchSnapshot();
|
||||
|
@ -86,10 +98,8 @@ describe('FlyoutService', () => {
|
|||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("on a stale FlyoutRef doesn't affect the active flyout", async () => {
|
||||
const target = document.createElement('div');
|
||||
const flyoutService = new FlyoutService(target);
|
||||
const ref1 = flyoutService.openFlyout(i18nMock, <span>Flyout content 1</span>);
|
||||
const ref2 = flyoutService.openFlyout(i18nMock, <span>Flyout content 2</span>);
|
||||
const ref1 = flyouts.open(mountText('Flyout content 1'));
|
||||
const ref2 = flyouts.open(mountText('Flyout content 2'));
|
||||
const onCloseComplete = jest.fn();
|
||||
ref2.onClose.then(onCloseComplete);
|
||||
mockReactDomUnmount.mockClear();
|
|
@ -23,8 +23,10 @@ import { EuiFlyout } from '@elastic/eui';
|
|||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subject } from 'rxjs';
|
||||
import { I18nStart } from '../i18n';
|
||||
import { OverlayRef } from './overlay_service';
|
||||
import { I18nStart } from '../../i18n';
|
||||
import { MountPoint } from '../../types';
|
||||
import { OverlayRef } from '../types';
|
||||
import { MountWrapper } from '../../utils';
|
||||
|
||||
/**
|
||||
* A FlyoutRef is a reference to an opened flyout panel. It offers methods to
|
||||
|
@ -37,7 +39,7 @@ import { OverlayRef } from './overlay_service';
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
export class FlyoutRef implements OverlayRef {
|
||||
class FlyoutRef implements OverlayRef {
|
||||
/**
|
||||
* An Promise that will resolve once this flyout is closed.
|
||||
*
|
||||
|
@ -66,55 +68,77 @@ export class FlyoutRef implements OverlayRef {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APIs to open and manage fly-out dialogs.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface OverlayFlyoutStart {
|
||||
/**
|
||||
* Opens a flyout panel with the given mount point inside. You can use
|
||||
* `close()` on the returned FlyoutRef to close the flyout.
|
||||
*
|
||||
* @param mount {@link MountPoint} - Mounts the children inside a flyout panel
|
||||
* @param options {@link OverlayFlyoutOpenOptions} - options for the flyout
|
||||
* @return {@link OverlayRef} A reference to the opened flyout panel.
|
||||
*/
|
||||
open(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface OverlayFlyoutOpenOptions {
|
||||
className?: string;
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
i18n: I18nStart;
|
||||
targetDomElement: Element;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class FlyoutService {
|
||||
private activeFlyout: FlyoutRef | null = null;
|
||||
private targetDomElement: Element | null = null;
|
||||
|
||||
constructor(private readonly targetDomElement: Element) {}
|
||||
public start({ i18n, targetDomElement }: StartDeps): OverlayFlyoutStart {
|
||||
this.targetDomElement = targetDomElement;
|
||||
|
||||
/**
|
||||
* Opens a flyout panel with the given component inside. You can use
|
||||
* `close()` on the returned FlyoutRef to close the flyout.
|
||||
*
|
||||
* @param flyoutChildren - Mounts the children inside a flyout panel
|
||||
* @return {FlyoutRef} A reference to the opened flyout panel.
|
||||
*/
|
||||
public openFlyout = (
|
||||
i18n: I18nStart,
|
||||
flyoutChildren: React.ReactNode,
|
||||
flyoutProps: {
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
} = {}
|
||||
): FlyoutRef => {
|
||||
// If there is an active flyout session close it before opening a new one.
|
||||
if (this.activeFlyout) {
|
||||
this.activeFlyout.close();
|
||||
this.cleanupDom();
|
||||
}
|
||||
return {
|
||||
open: (mount: MountPoint, options: OverlayFlyoutOpenOptions = {}): OverlayRef => {
|
||||
// If there is an active flyout session close it before opening a new one.
|
||||
if (this.activeFlyout) {
|
||||
this.activeFlyout.close();
|
||||
this.cleanupDom();
|
||||
}
|
||||
|
||||
const flyout = new FlyoutRef();
|
||||
const flyout = new FlyoutRef();
|
||||
|
||||
// If a flyout gets closed through it's FlyoutRef, remove it from the dom
|
||||
flyout.onClose.then(() => {
|
||||
if (this.activeFlyout === flyout) {
|
||||
this.cleanupDom();
|
||||
}
|
||||
});
|
||||
// If a flyout gets closed through it's FlyoutRef, remove it from the dom
|
||||
flyout.onClose.then(() => {
|
||||
if (this.activeFlyout === flyout) {
|
||||
this.cleanupDom();
|
||||
}
|
||||
});
|
||||
|
||||
this.activeFlyout = flyout;
|
||||
this.activeFlyout = flyout;
|
||||
|
||||
render(
|
||||
<i18n.Context>
|
||||
<EuiFlyout {...flyoutProps} onClose={() => flyout.close()}>
|
||||
{flyoutChildren}
|
||||
</EuiFlyout>
|
||||
</i18n.Context>,
|
||||
this.targetDomElement
|
||||
);
|
||||
render(
|
||||
<i18n.Context>
|
||||
<EuiFlyout {...options} onClose={() => flyout.close()}>
|
||||
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
|
||||
</EuiFlyout>
|
||||
</i18n.Context>,
|
||||
this.targetDomElement
|
||||
);
|
||||
|
||||
return flyout;
|
||||
};
|
||||
return flyout;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Using React.Render to re-render into a target DOM element will replace
|
||||
|
@ -124,8 +148,10 @@ export class FlyoutService {
|
|||
* depend on unmounting for cleanup behaviour.
|
||||
*/
|
||||
private cleanupDom(): void {
|
||||
unmountComponentAtNode(this.targetDomElement);
|
||||
this.targetDomElement.innerHTML = '';
|
||||
if (this.targetDomElement != null) {
|
||||
unmountComponentAtNode(this.targetDomElement);
|
||||
this.targetDomElement.innerHTML = '';
|
||||
}
|
||||
this.activeFlyout = null;
|
||||
}
|
||||
}
|
20
src/core/public/overlays/flyout/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { FlyoutService, OverlayFlyoutStart, OverlayFlyoutOpenOptions } from './flyout_service';
|
|
@ -17,5 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { OverlayRef } from './types';
|
||||
export { OverlayBannersStart } from './banners';
|
||||
export { OverlayService, OverlayStart, OverlayRef } from './overlay_service';
|
||||
export { OverlayFlyoutStart, OverlayFlyoutOpenOptions } from './flyout';
|
||||
export { OverlayModalStart, OverlayModalOpenOptions } from './modal';
|
||||
export { OverlayService, OverlayStart } from './overlay_service';
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subject } from 'rxjs';
|
||||
import { I18nStart } from '../i18n';
|
||||
import { OverlayRef } from './overlay_service';
|
||||
|
||||
/**
|
||||
* A ModalRef is a reference to an opened modal. It offers methods to
|
||||
* close the modal.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class ModalRef implements OverlayRef {
|
||||
public readonly onClose: Promise<void>;
|
||||
|
||||
private closeSubject = new Subject<void>();
|
||||
|
||||
constructor() {
|
||||
this.onClose = this.closeSubject.toPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the referenced modal if it's still open which in turn will
|
||||
* resolve the `onClose` Promise. If the modal had already been
|
||||
* closed this method does nothing.
|
||||
*/
|
||||
public close(): Promise<void> {
|
||||
if (!this.closeSubject.closed) {
|
||||
this.closeSubject.next();
|
||||
this.closeSubject.complete();
|
||||
}
|
||||
return this.onClose;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ModalService {
|
||||
private activeModal: ModalRef | null = null;
|
||||
|
||||
constructor(private readonly targetDomElement: Element) {}
|
||||
|
||||
/**
|
||||
* Opens a flyout panel with the given component inside. You can use
|
||||
* `close()` on the returned FlyoutRef to close the flyout.
|
||||
*
|
||||
* @param flyoutChildren - Mounts the children inside a flyout panel
|
||||
* @return {FlyoutRef} A reference to the opened flyout panel.
|
||||
*/
|
||||
public openModal = (
|
||||
i18n: I18nStart,
|
||||
modalChildren: React.ReactNode,
|
||||
modalProps: {
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
} = {}
|
||||
): ModalRef => {
|
||||
// If there is an active flyout session close it before opening a new one.
|
||||
if (this.activeModal) {
|
||||
this.activeModal.close();
|
||||
this.cleanupDom();
|
||||
}
|
||||
|
||||
const modal = new ModalRef();
|
||||
|
||||
// If a modal gets closed through it's ModalRef, remove it from the dom
|
||||
modal.onClose.then(() => {
|
||||
if (this.activeModal === modal) {
|
||||
this.cleanupDom();
|
||||
}
|
||||
});
|
||||
|
||||
this.activeModal = modal;
|
||||
|
||||
render(
|
||||
<EuiOverlayMask>
|
||||
<i18n.Context>
|
||||
<EuiModal {...modalProps} onClose={() => modal.close()}>
|
||||
{modalChildren}
|
||||
</EuiModal>
|
||||
</i18n.Context>
|
||||
</EuiOverlayMask>,
|
||||
this.targetDomElement
|
||||
);
|
||||
|
||||
return modal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Using React.Render to re-render into a target DOM element will replace
|
||||
* the content of the target but won't call unmountComponent on any
|
||||
* components inside the target or any of their children. So we properly
|
||||
* cleanup the DOM here to prevent subtle bugs in child components which
|
||||
* depend on unmounting for cleanup behaviour.
|
||||
*/
|
||||
private cleanupDom(): void {
|
||||
unmountComponentAtNode(this.targetDomElement);
|
||||
this.targetDomElement.innerHTML = '';
|
||||
this.activeModal = null;
|
||||
}
|
||||
}
|
69
src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ModalService ModalRef#close() can be called multiple times on the same ModalRef 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openModal() renders a modal to the DOM 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ModalService openModal() renders a modal to the DOM 2`] = `"<div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"false\\"><div class=\\"euiModal euiModal--maxWidth-default\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiModal__closeIcon\\" type=\\"button\\" aria-label=\\"Closes this modal window\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"></svg></button><div class=\\"euiModal__flex\\"><div class=\\"kbnOverlayMountWrapper\\"><span>Modal content</span></div></div></div></div><div data-focus-guard=\\"true\\" tabindex=\\"0\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div>"`;
|
||||
|
||||
exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
Array [
|
||||
<EuiOverlayMask>
|
||||
<mockConstructor>
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
>
|
||||
<MountWrapper
|
||||
className="kbnOverlayMountWrapper"
|
||||
mount={[Function]}
|
||||
/>
|
||||
</EuiModal>
|
||||
</mockConstructor>
|
||||
</EuiOverlayMask>,
|
||||
<div />,
|
||||
],
|
||||
]
|
||||
`;
|
20
src/core/public/overlays/modal/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { ModalService, OverlayModalStart, OverlayModalOpenOptions } from './modal_service';
|
|
@ -17,9 +17,27 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { TimeRange } from '../../../../../../plugins/data/public';
|
||||
import { Adapters } from '../../../../../../plugins/inspector/public';
|
||||
import { Query } from '../../../../../../plugins/data/public';
|
||||
import { ModalService, OverlayModalStart } from './modal_service';
|
||||
|
||||
export { TimeRange, Adapters, Query };
|
||||
export * from '../../../../../../plugins/expressions/public';
|
||||
const createStartContractMock = () => {
|
||||
const startContract: jest.Mocked<OverlayModalStart> = {
|
||||
open: jest.fn().mockReturnValue({
|
||||
close: jest.fn(),
|
||||
onClose: Promise.resolve(),
|
||||
}),
|
||||
};
|
||||
return startContract;
|
||||
};
|
||||
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<PublicMethodsOf<ModalService>> = {
|
||||
start: jest.fn(),
|
||||
};
|
||||
mocked.start.mockReturnValue(createStartContractMock());
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const overlayModalServiceMock = {
|
||||
create: createMock,
|
||||
createStartContract: createStartContractMock,
|
||||
};
|
|
@ -16,11 +16,14 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { mockReactDomRender, mockReactDomUnmount } from './flyout.test.mocks';
|
||||
import { mockReactDomRender, mockReactDomUnmount } from '../overlay.test.mocks';
|
||||
|
||||
import React from 'react';
|
||||
import { i18nServiceMock } from '../i18n/i18n_service.mock';
|
||||
import { ModalService, ModalRef } from './modal';
|
||||
import { mount } from 'enzyme';
|
||||
import { i18nServiceMock } from '../../i18n/i18n_service.mock';
|
||||
import { ModalService, OverlayModalStart } from './modal_service';
|
||||
import { mountReactNode } from '../../utils';
|
||||
import { OverlayRef } from '../types';
|
||||
|
||||
const i18nMock = i18nServiceMock.createStartContract();
|
||||
|
||||
|
@ -29,45 +32,59 @@ beforeEach(() => {
|
|||
mockReactDomUnmount.mockClear();
|
||||
});
|
||||
|
||||
const getServiceStart = () => {
|
||||
const service = new ModalService();
|
||||
return service.start({ i18n: i18nMock, targetDomElement: document.createElement('div') });
|
||||
};
|
||||
|
||||
describe('ModalService', () => {
|
||||
let modals: OverlayModalStart;
|
||||
beforeEach(() => {
|
||||
modals = getServiceStart();
|
||||
});
|
||||
|
||||
describe('openModal()', () => {
|
||||
it('renders a modal to the DOM', () => {
|
||||
const target = document.createElement('div');
|
||||
const modalService = new ModalService(target);
|
||||
expect(mockReactDomRender).not.toHaveBeenCalled();
|
||||
modalService.openModal(i18nMock, <span>Modal content</span>);
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
});
|
||||
describe('with a currently active modal', () => {
|
||||
let target: HTMLElement;
|
||||
let modalService: ModalService;
|
||||
let ref1: ModalRef;
|
||||
beforeEach(() => {
|
||||
target = document.createElement('div');
|
||||
modalService = new ModalService(target);
|
||||
ref1 = modalService.openModal(i18nMock, <span>Modal content 1</span>);
|
||||
modals.open(container => {
|
||||
const content = document.createElement('span');
|
||||
content.textContent = 'Modal content';
|
||||
container.append(content);
|
||||
return () => {};
|
||||
});
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
const modalContent = mount(mockReactDomRender.mock.calls[0][0]);
|
||||
expect(modalContent.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('with a currently active modal', () => {
|
||||
let ref1: OverlayRef;
|
||||
|
||||
beforeEach(() => {
|
||||
ref1 = modals.open(mountReactNode(<span>Modal content 1</span>));
|
||||
});
|
||||
|
||||
it('replaces the current modal with a new one', () => {
|
||||
modalService.openModal(i18nMock, <span>Flyout content 2</span>);
|
||||
modals.open(mountReactNode(<span>Flyout content 2</span>));
|
||||
expect(mockReactDomRender.mock.calls).toMatchSnapshot();
|
||||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
expect(() => ref1.close()).not.toThrowError();
|
||||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('resolves onClose on the previous ref', async () => {
|
||||
const onCloseComplete = jest.fn();
|
||||
ref1.onClose.then(onCloseComplete);
|
||||
modalService.openModal(i18nMock, <span>Flyout content 2</span>);
|
||||
modals.open(mountReactNode(<span>Flyout content 2</span>));
|
||||
await ref1.onClose;
|
||||
expect(onCloseComplete).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModalRef#close()', () => {
|
||||
it('resolves the onClose Promise', async () => {
|
||||
const target = document.createElement('div');
|
||||
const modalService = new ModalService(target);
|
||||
const ref = modalService.openModal(i18nMock, <span>Flyout content</span>);
|
||||
const ref = modals.open(mountReactNode(<span>Flyout content</span>));
|
||||
|
||||
const onCloseComplete = jest.fn();
|
||||
ref.onClose.then(onCloseComplete);
|
||||
|
@ -75,21 +92,19 @@ describe('ModalService', () => {
|
|||
await ref.close();
|
||||
expect(onCloseComplete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('can be called multiple times on the same ModalRef', async () => {
|
||||
const target = document.createElement('div');
|
||||
const modalService = new ModalService(target);
|
||||
const ref = modalService.openModal(i18nMock, <span>Flyout content</span>);
|
||||
const ref = modals.open(mountReactNode(<span>Flyout content</span>));
|
||||
expect(mockReactDomUnmount).not.toHaveBeenCalled();
|
||||
await ref.close();
|
||||
expect(mockReactDomUnmount.mock.calls).toMatchSnapshot();
|
||||
await ref.close();
|
||||
expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("on a stale ModalRef doesn't affect the active flyout", async () => {
|
||||
const target = document.createElement('div');
|
||||
const modalService = new ModalService(target);
|
||||
const ref1 = modalService.openModal(i18nMock, <span>Modal content 1</span>);
|
||||
const ref2 = modalService.openModal(i18nMock, <span>Modal content 2</span>);
|
||||
const ref1 = modals.open(mountReactNode(<span>Modal content 1</span>));
|
||||
const ref2 = modals.open(mountReactNode(<span>Modal content 2</span>));
|
||||
const onCloseComplete = jest.fn();
|
||||
ref2.onClose.then(onCloseComplete);
|
||||
mockReactDomUnmount.mockClear();
|
148
src/core/public/overlays/modal/modal_service.tsx
Normal file
|
@ -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.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subject } from 'rxjs';
|
||||
import { I18nStart } from '../../i18n';
|
||||
import { MountPoint } from '../../types';
|
||||
import { OverlayRef } from '../types';
|
||||
import { MountWrapper } from '../../utils';
|
||||
|
||||
/**
|
||||
* A ModalRef is a reference to an opened modal. It offers methods to
|
||||
* close the modal.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
class ModalRef implements OverlayRef {
|
||||
public readonly onClose: Promise<void>;
|
||||
|
||||
private closeSubject = new Subject<void>();
|
||||
|
||||
constructor() {
|
||||
this.onClose = this.closeSubject.toPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the referenced modal if it's still open which in turn will
|
||||
* resolve the `onClose` Promise. If the modal had already been
|
||||
* closed this method does nothing.
|
||||
*/
|
||||
public close(): Promise<void> {
|
||||
if (!this.closeSubject.closed) {
|
||||
this.closeSubject.next();
|
||||
this.closeSubject.complete();
|
||||
}
|
||||
return this.onClose;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APIs to open and manage modal dialogs.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface OverlayModalStart {
|
||||
/**
|
||||
* Opens a modal panel with the given mount point inside. You can use
|
||||
* `close()` on the returned OverlayRef to close the modal.
|
||||
*
|
||||
* @param mount {@link MountPoint} - Mounts the children inside the modal
|
||||
* @param options {@link OverlayModalOpenOptions} - options for the modal
|
||||
* @return {@link OverlayRef} A reference to the opened modal.
|
||||
*/
|
||||
open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface OverlayModalOpenOptions {
|
||||
className?: string;
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
i18n: I18nStart;
|
||||
targetDomElement: Element;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class ModalService {
|
||||
private activeModal: ModalRef | null = null;
|
||||
private targetDomElement: Element | null = null;
|
||||
|
||||
public start({ i18n, targetDomElement }: StartDeps): OverlayModalStart {
|
||||
this.targetDomElement = targetDomElement;
|
||||
|
||||
return {
|
||||
open: (mount: MountPoint, options: OverlayModalOpenOptions = {}): OverlayRef => {
|
||||
// If there is an active flyout session close it before opening a new one.
|
||||
if (this.activeModal) {
|
||||
this.activeModal.close();
|
||||
this.cleanupDom();
|
||||
}
|
||||
|
||||
const modal = new ModalRef();
|
||||
|
||||
// If a modal gets closed through it's ModalRef, remove it from the dom
|
||||
modal.onClose.then(() => {
|
||||
if (this.activeModal === modal) {
|
||||
this.cleanupDom();
|
||||
}
|
||||
});
|
||||
|
||||
this.activeModal = modal;
|
||||
|
||||
render(
|
||||
<EuiOverlayMask>
|
||||
<i18n.Context>
|
||||
<EuiModal {...options} onClose={() => modal.close()}>
|
||||
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
|
||||
</EuiModal>
|
||||
</i18n.Context>
|
||||
</EuiOverlayMask>,
|
||||
targetDomElement
|
||||
);
|
||||
|
||||
return modal;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Using React.Render to re-render into a target DOM element will replace
|
||||
* the content of the target but won't call unmountComponent on any
|
||||
* components inside the target or any of their children. So we properly
|
||||
* cleanup the DOM here to prevent subtle bugs in child components which
|
||||
* depend on unmounting for cleanup behaviour.
|
||||
*/
|
||||
private cleanupDom(): void {
|
||||
if (this.targetDomElement != null) {
|
||||
unmountComponentAtNode(this.targetDomElement);
|
||||
this.targetDomElement.innerHTML = '';
|
||||
}
|
||||
this.activeModal = null;
|
||||
}
|
||||
}
|
|
@ -19,7 +19,9 @@
|
|||
|
||||
export const mockReactDomRender = jest.fn();
|
||||
export const mockReactDomUnmount = jest.fn();
|
||||
export const mockReactDomCreatePortal = jest.fn().mockImplementation(component => component);
|
||||
jest.doMock('react-dom', () => ({
|
||||
render: mockReactDomRender,
|
||||
createPortal: mockReactDomCreatePortal,
|
||||
unmountComponentAtNode: mockReactDomUnmount,
|
||||
}));
|
|
@ -18,17 +18,15 @@
|
|||
*/
|
||||
import { OverlayService, OverlayStart } from './overlay_service';
|
||||
import { overlayBannersServiceMock } from './banners/banners_service.mock';
|
||||
import { overlayFlyoutServiceMock } from './flyout/flyout_service.mock';
|
||||
import { overlayModalServiceMock } from './modal/modal_service.mock';
|
||||
|
||||
const createStartContractMock = () => {
|
||||
const startContract: DeeplyMockedKeys<OverlayStart> = {
|
||||
openFlyout: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
openFlyout: overlayFlyoutServiceMock.createStartContract().open,
|
||||
openModal: overlayModalServiceMock.createStartContract().open,
|
||||
banners: overlayBannersServiceMock.createStartContract(),
|
||||
};
|
||||
startContract.openModal.mockReturnValue({
|
||||
close: jest.fn(),
|
||||
onClose: Promise.resolve(),
|
||||
});
|
||||
return startContract;
|
||||
};
|
||||
|
||||
|
|
|
@ -17,34 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { FlyoutService } from './flyout';
|
||||
import { ModalService } from './modal';
|
||||
import { I18nStart } from '../i18n';
|
||||
import { OverlayBannersStart, OverlayBannersService } from './banners';
|
||||
import { UiSettingsClientContract } from '../ui_settings';
|
||||
|
||||
/**
|
||||
* Returned by {@link OverlayStart} methods for closing a mounted overlay.
|
||||
* @public
|
||||
*/
|
||||
export interface OverlayRef {
|
||||
/**
|
||||
* A Promise that will resolve once this overlay is closed.
|
||||
*
|
||||
* Overlays can close from user interaction, calling `close()` on the overlay
|
||||
* reference or another overlay replacing yours via `openModal` or `openFlyout`.
|
||||
*/
|
||||
onClose: Promise<void>;
|
||||
|
||||
/**
|
||||
* Closes the referenced overlay if it's still open which in turn will
|
||||
* resolve the `onClose` Promise. If the overlay had already been
|
||||
* closed this method does nothing.
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
}
|
||||
import { OverlayBannersStart, OverlayBannersService } from './banners';
|
||||
import { FlyoutService, OverlayFlyoutStart } from './flyout';
|
||||
import { ModalService, OverlayModalStart } from './modal';
|
||||
|
||||
interface StartDeps {
|
||||
i18n: I18nStart;
|
||||
|
@ -54,19 +31,25 @@ interface StartDeps {
|
|||
|
||||
/** @internal */
|
||||
export class OverlayService {
|
||||
private bannersService = new OverlayBannersService();
|
||||
private modalService = new ModalService();
|
||||
private flyoutService = new FlyoutService();
|
||||
|
||||
public start({ i18n, targetDomElement, uiSettings }: StartDeps): OverlayStart {
|
||||
const flyoutElement = document.createElement('div');
|
||||
const modalElement = document.createElement('div');
|
||||
targetDomElement.appendChild(flyoutElement);
|
||||
const flyouts = this.flyoutService.start({ i18n, targetDomElement: flyoutElement });
|
||||
|
||||
const banners = this.bannersService.start({ i18n, uiSettings });
|
||||
|
||||
const modalElement = document.createElement('div');
|
||||
targetDomElement.appendChild(modalElement);
|
||||
const flyoutService = new FlyoutService(flyoutElement);
|
||||
const modalService = new ModalService(modalElement);
|
||||
const bannersService = new OverlayBannersService();
|
||||
const modals = this.modalService.start({ i18n, targetDomElement: modalElement });
|
||||
|
||||
return {
|
||||
banners: bannersService.start({ i18n, uiSettings }),
|
||||
openFlyout: flyoutService.openFlyout.bind(flyoutService, i18n),
|
||||
openModal: modalService.openModal.bind(modalService, i18n),
|
||||
banners,
|
||||
openFlyout: flyouts.open.bind(flyouts),
|
||||
openModal: modals.open.bind(modals),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -75,19 +58,8 @@ export class OverlayService {
|
|||
export interface OverlayStart {
|
||||
/** {@link OverlayBannersStart} */
|
||||
banners: OverlayBannersStart;
|
||||
openFlyout: (
|
||||
flyoutChildren: React.ReactNode,
|
||||
flyoutProps?: {
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
) => OverlayRef;
|
||||
openModal: (
|
||||
modalChildren: React.ReactNode,
|
||||
modalProps?: {
|
||||
className?: string;
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
) => OverlayRef;
|
||||
/** {@link OverlayFlyoutStart#open} */
|
||||
openFlyout: OverlayFlyoutStart['open'];
|
||||
/** {@link OverlayModalStart#open} */
|
||||
openModal: OverlayModalStart['open'];
|
||||
}
|
||||
|
|
|
@ -17,15 +17,23 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { nodeTypes } from '../node_types';
|
||||
/**
|
||||
* Returned by {@link OverlayStart} methods for closing a mounted overlay.
|
||||
* @public
|
||||
*/
|
||||
export interface OverlayRef {
|
||||
/**
|
||||
* A Promise that will resolve once this overlay is closed.
|
||||
*
|
||||
* Overlays can close from user interaction, calling `close()` on the overlay
|
||||
* reference or another overlay replacing yours via `openModal` or `openFlyout`.
|
||||
*/
|
||||
onClose: Promise<void>;
|
||||
|
||||
export function convertGeoBoundingBox(filter) {
|
||||
if (filter.meta.type !== 'geo_bounding_box') {
|
||||
throw new Error(`Expected filter of type "geo_bounding_box", got "${filter.meta.type}"`);
|
||||
}
|
||||
|
||||
const { key, params } = filter.meta;
|
||||
const camelParams = _.mapKeys(params, (value, key) => _.camelCase(key));
|
||||
return nodeTypes.function.buildNode('geoBoundingBox', key, camelParams);
|
||||
/**
|
||||
* Closes the referenced overlay if it's still open which in turn will
|
||||
* resolve the `onClose` Promise. If the overlay had already been
|
||||
* closed this method does nothing.
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
}
|
|
@ -124,7 +124,7 @@ export type ChromeHelpExtension = (element: HTMLDivElement) => () => void;
|
|||
// @public (undocumented)
|
||||
export interface ChromeNavControl {
|
||||
// (undocumented)
|
||||
mount(targetDomElement: HTMLElement): () => void;
|
||||
mount: MountPoint;
|
||||
// (undocumented)
|
||||
order?: number;
|
||||
}
|
||||
|
@ -620,7 +620,7 @@ export interface LegacyNavLink {
|
|||
}
|
||||
|
||||
// @public
|
||||
export type MountPoint = (element: HTMLElement) => UnmountCallback;
|
||||
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface NotificationsSetup {
|
||||
|
@ -657,17 +657,14 @@ export interface OverlayRef {
|
|||
export interface OverlayStart {
|
||||
// (undocumented)
|
||||
banners: OverlayBannersStart;
|
||||
// Warning: (ae-forgotten-export) The symbol "OverlayFlyoutStart" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
openFlyout: (flyoutChildren: React.ReactNode, flyoutProps?: {
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
}) => OverlayRef;
|
||||
openFlyout: OverlayFlyoutStart['open'];
|
||||
// Warning: (ae-forgotten-export) The symbol "OverlayModalStart" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
openModal: (modalChildren: React.ReactNode, modalProps?: {
|
||||
className?: string;
|
||||
closeButtonAriaLabel?: string;
|
||||
'data-test-subj'?: string;
|
||||
}) => OverlayRef;
|
||||
openModal: OverlayModalStart['open'];
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -941,9 +938,12 @@ export class ToastsApi implements IToasts {
|
|||
addSuccess(toastOrTitle: ToastInput): Toast;
|
||||
addWarning(toastOrTitle: ToastInput): Toast;
|
||||
get$(): Rx.Observable<Toast[]>;
|
||||
// @internal (undocumented)
|
||||
registerOverlays(overlays: OverlayStart): void;
|
||||
remove(toastOrId: Toast | string): void;
|
||||
// @internal (undocumented)
|
||||
start({ overlays, i18n }: {
|
||||
overlays: OverlayStart;
|
||||
i18n: I18nStart;
|
||||
}): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
export type MountPoint = (element: HTMLElement) => UnmountCallback;
|
||||
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
|
||||
|
||||
/**
|
||||
* A function that will unmount the element previously mounted by
|
||||
|
|
79
src/core/public/utils/mount.test.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES 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 } from 'enzyme';
|
||||
import { MountWrapper, mountReactNode } from './mount';
|
||||
|
||||
describe('MountWrapper', () => {
|
||||
it('renders an html element in react tree', () => {
|
||||
const mountPoint = (container: HTMLElement) => {
|
||||
const el = document.createElement('p');
|
||||
el.textContent = 'hello';
|
||||
el.className = 'bar';
|
||||
container.append(el);
|
||||
return () => {};
|
||||
};
|
||||
const wrapper = <MountWrapper mount={mountPoint} />;
|
||||
const container = mount(wrapper);
|
||||
expect(container.html()).toMatchInlineSnapshot(
|
||||
`"<div class=\\"kbnMountWrapper\\"><p class=\\"bar\\">hello</p></div>"`
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the react tree when the mounted element changes', () => {
|
||||
const el = document.createElement('p');
|
||||
el.textContent = 'initial';
|
||||
|
||||
const mountPoint = (container: HTMLElement) => {
|
||||
container.append(el);
|
||||
return () => {};
|
||||
};
|
||||
|
||||
const wrapper = <MountWrapper mount={mountPoint} />;
|
||||
const container = mount(wrapper);
|
||||
expect(container.html()).toMatchInlineSnapshot(
|
||||
`"<div class=\\"kbnMountWrapper\\"><p>initial</p></div>"`
|
||||
);
|
||||
|
||||
el.textContent = 'changed';
|
||||
container.update();
|
||||
expect(container.html()).toMatchInlineSnapshot(
|
||||
`"<div class=\\"kbnMountWrapper\\"><p>changed</p></div>"`
|
||||
);
|
||||
});
|
||||
|
||||
it('can render a detached react component', () => {
|
||||
const mountPoint = mountReactNode(<span>detached</span>);
|
||||
const wrapper = <MountWrapper mount={mountPoint} />;
|
||||
const container = mount(wrapper);
|
||||
expect(container.html()).toMatchInlineSnapshot(
|
||||
`"<div class=\\"kbnMountWrapper\\"><span>detached</span></div>"`
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts a className prop to override default className', () => {
|
||||
const mountPoint = mountReactNode(<span>detached</span>);
|
||||
const wrapper = <MountWrapper mount={mountPoint} className="customClass" />;
|
||||
const container = mount(wrapper);
|
||||
expect(container.html()).toMatchInlineSnapshot(
|
||||
`"<div class=\\"customClass\\"><span>detached</span></div>"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -22,23 +22,26 @@ import { render, unmountComponentAtNode } from 'react-dom';
|
|||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { MountPoint } from '../types';
|
||||
|
||||
const defaultWrapperClass = 'kbnMountWrapper';
|
||||
|
||||
/**
|
||||
* MountWrapper is a react component to mount a {@link MountPoint} inside a react tree.
|
||||
*/
|
||||
export const MountWrapper: React.FunctionComponent<{ mount: MountPoint }> = ({ mount }) => {
|
||||
export const MountWrapper: React.FunctionComponent<{ mount: MountPoint; className?: string }> = ({
|
||||
mount,
|
||||
className = defaultWrapperClass,
|
||||
}) => {
|
||||
const element = useRef(null);
|
||||
useEffect(() => mount(element.current!), [mount]);
|
||||
return <div className="kbnMountWrapper" ref={element} />;
|
||||
return <div className={className} ref={element} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mount converter for react components.
|
||||
* Mount converter for react node.
|
||||
*
|
||||
* @param component to get a mount for
|
||||
* @param node to get a mount for
|
||||
*/
|
||||
export const mountReactNode = (component: React.ReactNode): MountPoint => (
|
||||
element: HTMLElement
|
||||
) => {
|
||||
render(<I18nProvider>{component}</I18nProvider>, element);
|
||||
export const mountReactNode = (node: React.ReactNode): MountPoint => (element: HTMLElement) => {
|
||||
render(<I18nProvider>{node}</I18nProvider>, element);
|
||||
return () => unmountComponentAtNode(element);
|
||||
};
|
||||
|
|
|
@ -55,7 +55,7 @@ test('throws if config at path does not match schema', async () => {
|
|||
await expect(
|
||||
configService.setSchema('key', schema.string())
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"[key]: expected value of type [string] but got [number]"`
|
||||
`"[config validation of [key]]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -78,11 +78,11 @@ test('re-validate config when updated', async () => {
|
|||
config$.next(new ObjectToConfigAdapter({ key: 123 }));
|
||||
|
||||
await expect(valuesReceived).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"value",
|
||||
[Error: [key]: expected value of type [string] but got [number]],
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"value",
|
||||
[Error: [config validation of [key]]: expected value of type [string] but got [number]],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("returns undefined if fetching optional config at a path that doesn't exist", async () => {
|
||||
|
@ -143,7 +143,7 @@ test("throws error if 'schema' is not defined for a key", async () => {
|
|||
const configs = configService.atPath('key');
|
||||
|
||||
await expect(configs.pipe(first()).toPromise()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: No validation schema has been defined for key]`
|
||||
`[Error: No validation schema has been defined for [key]]`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -153,7 +153,7 @@ test("throws error if 'setSchema' called several times for the same key", async
|
|||
const addSchema = async () => await configService.setSchema('key', schema.string());
|
||||
await addSchema();
|
||||
await expect(addSchema()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Validation schema for key was already registered.]`
|
||||
`[Error: Validation schema for [key] was already registered.]`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -280,6 +280,33 @@ test('handles disabled path and marks config as used', async () => {
|
|||
expect(unusedPaths).toEqual([]);
|
||||
});
|
||||
|
||||
test('does not throw if schema does not define "enabled" schema', async () => {
|
||||
const initialConfig = {
|
||||
pid: {
|
||||
file: '/some/file.pid',
|
||||
},
|
||||
};
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
expect(
|
||||
configService.setSchema(
|
||||
'pid',
|
||||
schema.object({
|
||||
file: schema.string(),
|
||||
})
|
||||
)
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
const value$ = configService.atPath('pid');
|
||||
const value: any = await value$.pipe(first()).toPromise();
|
||||
expect(value.enabled).toBe(undefined);
|
||||
|
||||
const valueOptional$ = configService.optionalAtPath('pid');
|
||||
const valueOptional: any = await valueOptional$.pipe(first()).toPromise();
|
||||
expect(valueOptional.enabled).toBe(undefined);
|
||||
});
|
||||
|
||||
test('treats config as enabled if config path is not present in config', async () => {
|
||||
const initialConfig = {};
|
||||
|
||||
|
@ -292,3 +319,45 @@ test('treats config as enabled if config path is not present in config', async (
|
|||
const unusedPaths = await configService.getUnusedPaths();
|
||||
expect(unusedPaths).toEqual([]);
|
||||
});
|
||||
|
||||
test('read "enabled" even if its schema is not present', async () => {
|
||||
const initialConfig = {
|
||||
foo: {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const isEnabled = await configService.isEnabledAtPath('foo');
|
||||
expect(isEnabled).toBe(true);
|
||||
});
|
||||
|
||||
test('allows plugins to specify "enabled" flag via validation schema', async () => {
|
||||
const initialConfig = {};
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
await configService.setSchema(
|
||||
'foo',
|
||||
schema.object({ enabled: schema.boolean({ defaultValue: false }) })
|
||||
);
|
||||
|
||||
expect(await configService.isEnabledAtPath('foo')).toBe(false);
|
||||
|
||||
await configService.setSchema(
|
||||
'bar',
|
||||
schema.object({ enabled: schema.boolean({ defaultValue: true }) })
|
||||
);
|
||||
|
||||
expect(await configService.isEnabledAtPath('bar')).toBe(true);
|
||||
|
||||
await configService.setSchema(
|
||||
'baz',
|
||||
schema.object({ different: schema.boolean({ defaultValue: true }) })
|
||||
);
|
||||
|
||||
expect(await configService.isEnabledAtPath('baz')).toBe(true);
|
||||
});
|
||||
|
|
|
@ -54,7 +54,7 @@ export class ConfigService {
|
|||
public async setSchema(path: ConfigPath, schema: Type<unknown>) {
|
||||
const namespace = pathToString(path);
|
||||
if (this.schemas.has(namespace)) {
|
||||
throw new Error(`Validation schema for ${path} was already registered.`);
|
||||
throw new Error(`Validation schema for [${path}] was already registered.`);
|
||||
}
|
||||
|
||||
this.schemas.set(namespace, schema);
|
||||
|
@ -98,14 +98,28 @@ export class ConfigService {
|
|||
}
|
||||
|
||||
public async isEnabledAtPath(path: ConfigPath) {
|
||||
const enabledPath = createPluginEnabledPath(path);
|
||||
const namespace = pathToString(path);
|
||||
|
||||
const validatedConfig = this.schemas.has(namespace)
|
||||
? await this.atPath<{ enabled?: boolean }>(path)
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
: undefined;
|
||||
|
||||
const enabledPath = createPluginEnabledPath(path);
|
||||
const config = await this.config$.pipe(first()).toPromise();
|
||||
if (!config.has(enabledPath)) {
|
||||
|
||||
// if plugin hasn't got a config schema, we try to read "enabled" directly
|
||||
const isEnabled =
|
||||
validatedConfig && validatedConfig.enabled !== undefined
|
||||
? validatedConfig.enabled
|
||||
: config.get(enabledPath);
|
||||
|
||||
// not declared. consider that plugin is enabled by default
|
||||
if (isEnabled === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isEnabled = config.get(enabledPath);
|
||||
if (isEnabled === false) {
|
||||
// If the plugin is _not_ enabled, we mark the entire plugin path as
|
||||
// handled, as it's expected that it won't be used.
|
||||
|
@ -138,7 +152,7 @@ export class ConfigService {
|
|||
const namespace = pathToString(path);
|
||||
const schema = this.schemas.get(namespace);
|
||||
if (!schema) {
|
||||
throw new Error(`No validation schema has been defined for ${namespace}`);
|
||||
throw new Error(`No validation schema has been defined for [${namespace}]`);
|
||||
}
|
||||
return schema.validate(
|
||||
config,
|
||||
|
@ -147,7 +161,7 @@ export class ConfigService {
|
|||
prod: this.env.mode.prod,
|
||||
...this.env.packageInfo,
|
||||
},
|
||||
namespace
|
||||
`config validation of [${namespace}]`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
3
src/core/server/elasticsearch/__snapshots__/elasticsearch_config.test.ts.snap
generated
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`#username throws if equal to "elastic", only while running from source 1`] = `"[username]: value of \\"elastic\\" is forbidden. This is a superuser account that can obfuscate privilege-related issues. You should use the \\"kibana\\" user instead."`;
|
|
@ -107,3 +107,11 @@ test('#ssl.certificateAuthorities accepts both string and array of strings', ()
|
|||
);
|
||||
expect(configValue.ssl.certificateAuthorities).toEqual(['some-path', 'another-path']);
|
||||
});
|
||||
|
||||
test('#username throws if equal to "elastic", only while running from source', () => {
|
||||
const obj = {
|
||||
username: 'elastic',
|
||||
};
|
||||
expect(() => config.schema.validate(obj, { dist: false })).toThrowErrorMatchingSnapshot();
|
||||
expect(() => config.schema.validate(obj, { dist: true })).not.toThrow();
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { Duration } from 'moment';
|
||||
import { Logger } from '../logging';
|
||||
|
||||
const hostURISchema = schema.uri({ scheme: ['http', 'https'] });
|
||||
|
||||
|
@ -39,7 +40,23 @@ export const config = {
|
|||
defaultValue: 'http://localhost:9200',
|
||||
}),
|
||||
preserveHost: schema.boolean({ defaultValue: true }),
|
||||
username: schema.maybe(schema.string()),
|
||||
username: schema.maybe(
|
||||
schema.conditional(
|
||||
schema.contextRef('dist'),
|
||||
false,
|
||||
schema.string({
|
||||
validate: rawConfig => {
|
||||
if (rawConfig === 'elastic') {
|
||||
return (
|
||||
'value of "elastic" is forbidden. This is a superuser account that can obfuscate ' +
|
||||
'privilege-related issues. You should use the "kibana" user instead.'
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
schema.string()
|
||||
)
|
||||
),
|
||||
password: schema.maybe(schema.string()),
|
||||
requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], {
|
||||
defaultValue: ['authorization'],
|
||||
|
@ -166,7 +183,7 @@ export class ElasticsearchConfig {
|
|||
*/
|
||||
public readonly customHeaders: ElasticsearchConfigType['customHeaders'];
|
||||
|
||||
constructor(rawConfig: ElasticsearchConfigType) {
|
||||
constructor(rawConfig: ElasticsearchConfigType, log?: Logger) {
|
||||
this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch;
|
||||
this.apiVersion = rawConfig.apiVersion;
|
||||
this.logQueries = rawConfig.logQueries;
|
||||
|
@ -195,5 +212,14 @@ export class ElasticsearchConfig {
|
|||
...rawConfig.ssl,
|
||||
certificateAuthorities,
|
||||
};
|
||||
|
||||
if (this.username === 'elastic' && log !== undefined) {
|
||||
// logger is optional / not used during tests
|
||||
// TODO: logger can be removed when issue #40255 is resolved to support deprecations in NP config service
|
||||
log.warn(
|
||||
`Setting the elasticsearch username to "elastic" is deprecated. You should use the "kibana" user instead.`,
|
||||
{ tags: ['deprecation'] }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ export class ElasticsearchService implements CoreService<InternalElasticsearchSe
|
|||
this.log = coreContext.logger.get('elasticsearch-service');
|
||||
this.config$ = coreContext.configService
|
||||
.atPath<ElasticsearchConfigType>('elasticsearch')
|
||||
.pipe(map(rawConfig => new ElasticsearchConfig(rawConfig)));
|
||||
.pipe(map(rawConfig => new ElasticsearchConfig(rawConfig, coreContext.logger.get('config'))));
|
||||
}
|
||||
|
||||
public async setup(deps: SetupDeps): Promise<InternalElasticsearchServiceSetup> {
|
||||
|
|
|
@ -52,7 +52,7 @@ export type RequestHandlerContextProvider<
|
|||
*
|
||||
* @example
|
||||
* To handle an incoming request in your plugin you should:
|
||||
* - Create a `Router` instance. Router is already configured to use `plugin-id` to prefix path segment for your routes.
|
||||
* - Create a `Router` instance.
|
||||
* ```ts
|
||||
* const router = httpSetup.createRouter();
|
||||
* ```
|
||||
|
@ -87,7 +87,7 @@ export type RequestHandlerContextProvider<
|
|||
* }
|
||||
* ```
|
||||
*
|
||||
* - Register route handler for GET request to 'my-app/path/{id}' path
|
||||
* - Register route handler for GET request to 'path/{id}' path
|
||||
* ```ts
|
||||
* import { schema, TypeOf } from '@kbn/config-schema';
|
||||
* const router = httpSetup.createRouter();
|
||||
|
@ -184,7 +184,7 @@ export interface HttpServiceSetup {
|
|||
* @example
|
||||
* ```ts
|
||||
* const router = createRouter();
|
||||
* // handler is called when '${my-plugin-id}/path' resource is requested with `GET` method
|
||||
* // handler is called when '/path' resource is requested with `GET` method
|
||||
* router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' }));
|
||||
* ```
|
||||
* @public
|
||||
|
|
|
@ -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 { Poller } from './poller';
|
||||
|
||||
const delay = (duration: number) => new Promise(r => setTimeout(r, duration));
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/44560
|
||||
describe.skip('Poller', () => {
|
||||
let handler: jest.Mock<any, any>;
|
||||
let poller: Poller<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = jest.fn().mockImplementation((iteration: number) => `polling-${iteration}`);
|
||||
poller = new Poller<string>(100, 'polling', handler);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
poller.unsubscribe();
|
||||
});
|
||||
|
||||
it('returns an observable of subject', async () => {
|
||||
await delay(300);
|
||||
expect(poller.subject$.getValue()).toBe('polling-2');
|
||||
});
|
||||
|
||||
it('executes a function on an interval', async () => {
|
||||
await delay(300);
|
||||
expect(handler).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
it('no longer polls after unsubscribing', async () => {
|
||||
await delay(300);
|
||||
poller.unsubscribe();
|
||||
await delay(300);
|
||||
expect(handler).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
it('does not add next value if returns undefined', async () => {
|
||||
const values: any[] = [];
|
||||
const polling = new Poller<string>(100, 'polling', iteration => {
|
||||
if (iteration % 2 === 0) {
|
||||
return `polling-${iteration}`;
|
||||
}
|
||||
});
|
||||
|
||||
polling.subject$.subscribe(value => {
|
||||
values.push(value);
|
||||
});
|
||||
await delay(300);
|
||||
polling.unsubscribe();
|
||||
|
||||
expect(values).toEqual(['polling', 'polling-0', 'polling-2']);
|
||||
});
|
||||
});
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, timer } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Create an Observable BehaviorSubject to invoke a function on an interval
|
||||
* which returns the next value for the observable.
|
||||
* @public
|
||||
*/
|
||||
export class Poller<T> {
|
||||
/**
|
||||
* The observable to observe for changes to the poller value.
|
||||
*/
|
||||
public readonly subject$ = new BehaviorSubject<T>(this.initialValue);
|
||||
private poller$ = timer(0, this.frequency);
|
||||
private subscription = this.poller$.subscribe(async iteration => {
|
||||
const next = await this.handler(iteration);
|
||||
|
||||
if (next !== undefined) {
|
||||
this.subject$.next(next);
|
||||
}
|
||||
|
||||
return iteration;
|
||||
});
|
||||
|
||||
constructor(
|
||||
private frequency: number,
|
||||
private initialValue: T,
|
||||
private handler: (iteration: number) => Promise<T | undefined> | T | undefined
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Permanently end the polling operation.
|
||||
*/
|
||||
unsubscribe() {
|
||||
return this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
|
@ -17,9 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { castEsToKbnFieldTypeName } from '../plugins/data/common';
|
||||
// eslint-disable-next-line max-len
|
||||
import { shouldReadFieldFromDocValues } from '../plugins/data/server';
|
||||
import {
|
||||
shouldReadFieldFromDocValues,
|
||||
castEsToKbnFieldTypeName,
|
||||
} from '../plugins/data/server';
|
||||
|
||||
function stubbedLogstashFields() {
|
||||
return [
|
||||
|
|
|
@ -62,10 +62,6 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => {
|
|||
updateCurrentState: () => {},
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
ResizeChecker: function() {
|
||||
return { on: () => {} };
|
||||
},
|
||||
docLinkVersion: 'NA',
|
||||
};
|
||||
editor = mount(
|
||||
|
|
|
@ -63,7 +63,6 @@ const DEFAULT_INPUT_VALUE = `GET _search
|
|||
function _Editor({ previousStateLocation = 'stored' }: EditorProps) {
|
||||
const {
|
||||
services: { history, notifications },
|
||||
ResizeChecker,
|
||||
docLinkVersion,
|
||||
} = useAppContext();
|
||||
|
||||
|
@ -130,7 +129,6 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) {
|
|||
mappings.retrieveAutoCompleteInfo();
|
||||
|
||||
const unsubscribeResizer = subscribeResizeChecker(
|
||||
ResizeChecker,
|
||||
editorRef.current!,
|
||||
editorInstanceRef.current
|
||||
);
|
||||
|
|
|
@ -31,7 +31,6 @@ function _EditorOuput() {
|
|||
const editorInstanceRef = useRef<null | any>(null);
|
||||
const {
|
||||
services: { settings },
|
||||
ResizeChecker,
|
||||
} = useAppContext();
|
||||
|
||||
const dispatch = useEditorActionContext();
|
||||
|
@ -42,11 +41,7 @@ function _EditorOuput() {
|
|||
const editor$ = $(editorRef.current!);
|
||||
editorInstanceRef.current = initializeOutput(editor$, settings);
|
||||
editorInstanceRef.current.update('');
|
||||
const unsubscribe = subscribeResizeChecker(
|
||||
ResizeChecker,
|
||||
editorRef.current!,
|
||||
editorInstanceRef.current
|
||||
);
|
||||
const unsubscribe = subscribeResizeChecker(editorRef.current!, editorInstanceRef.current);
|
||||
|
||||
dispatch({ type: 'setOutputEditor', value: editorInstanceRef.current });
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ const CHILD_ELEMENT_PREFIX = 'historyReq';
|
|||
export function ConsoleHistory({ close }: Props) {
|
||||
const {
|
||||
services: { history },
|
||||
ResizeChecker,
|
||||
} = useAppContext();
|
||||
|
||||
const dispatch = useEditorActionContext();
|
||||
|
@ -200,7 +199,6 @@ export function ConsoleHistory({ close }: Props) {
|
|||
<HistoryViewer
|
||||
settings={readOnlySettings}
|
||||
req={viewingReq}
|
||||
ResizeChecker={ResizeChecker}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -31,10 +31,9 @@ import { applyCurrentSettings } from '../console_editor/apply_editor_settings';
|
|||
interface Props {
|
||||
settings: DevToolsSettings;
|
||||
req: any | null;
|
||||
ResizeChecker: any;
|
||||
}
|
||||
|
||||
export function HistoryViewer({ settings, ResizeChecker, req }: Props) {
|
||||
export function HistoryViewer({ settings, req }: Props) {
|
||||
const divRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewerRef = useRef<any | null>(null);
|
||||
|
||||
|
@ -43,7 +42,7 @@ export function HistoryViewer({ settings, ResizeChecker, req }: Props) {
|
|||
viewerRef.current = viewer;
|
||||
viewer.renderer.setShowPrintMargin(false);
|
||||
viewer.$blockScrolling = Infinity;
|
||||
const unsubscribe = subscribeResizeChecker(ResizeChecker, divRef.current!, viewer);
|
||||
const unsubscribe = subscribeResizeChecker(divRef.current!, viewer);
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -16,9 +16,10 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ResizeChecker } from '../../../../../../../../../plugins/kibana_utils/public';
|
||||
|
||||
export function subscribeResizeChecker(ResizeChecker: any, $el: any, ...editors: any[]) {
|
||||
const checker = new ResizeChecker($el);
|
||||
export function subscribeResizeChecker(el: HTMLElement, ...editors: any[]) {
|
||||
const checker = new ResizeChecker(el);
|
||||
checker.on('resize', () =>
|
||||
editors.forEach(e => {
|
||||
e.resize();
|
||||
|
|
|
@ -29,7 +29,6 @@ interface ContextValue {
|
|||
notifications: NotificationsSetup;
|
||||
};
|
||||
docLinkVersion: string;
|
||||
ResizeChecker: any;
|
||||
}
|
||||
|
||||
interface ContextProps {
|
||||
|
|
|
@ -32,10 +32,9 @@ export function legacyBackDoorToSettings() {
|
|||
export function boot(deps: {
|
||||
docLinkVersion: string;
|
||||
I18nContext: any;
|
||||
ResizeChecker: any;
|
||||
notifications: NotificationsSetup;
|
||||
}) {
|
||||
const { I18nContext, ResizeChecker, notifications, docLinkVersion } = deps;
|
||||
const { I18nContext, notifications, docLinkVersion } = deps;
|
||||
|
||||
const storage = createStorage({
|
||||
engine: window.localStorage,
|
||||
|
@ -51,7 +50,6 @@ export function boot(deps: {
|
|||
value={{
|
||||
docLinkVersion,
|
||||
services: { storage, history, settings, notifications },
|
||||
ResizeChecker,
|
||||
}}
|
||||
>
|
||||
<EditorContextProvider settings={settings.toJSON()}>
|
||||
|
|
|
@ -26,7 +26,6 @@ import 'brace/mode/text';
|
|||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
import { npSetup, npStart } from 'ui/new_platform';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { ResizeChecker } from 'ui/resize_checker';
|
||||
/* eslint-enable @kbn/eslint/no-restricted-paths */
|
||||
|
||||
export interface XPluginSet {
|
||||
|
@ -34,7 +33,6 @@ export interface XPluginSet {
|
|||
feature_catalogue: FeatureCatalogueSetup;
|
||||
__LEGACY: {
|
||||
I18nContext: any;
|
||||
ResizeChecker: any;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -48,7 +46,6 @@ pluginInstance.setup(npSetup.core, {
|
|||
...npSetup.plugins,
|
||||
__LEGACY: {
|
||||
I18nContext,
|
||||
ResizeChecker,
|
||||
},
|
||||
});
|
||||
pluginInstance.start(npStart.core);
|
||||
|
|
|
@ -30,7 +30,7 @@ export class ConsoleUIPlugin implements Plugin<any, any> {
|
|||
|
||||
async setup({ notifications }: CoreSetup, pluginSet: XPluginSet) {
|
||||
const {
|
||||
__LEGACY: { I18nContext, ResizeChecker },
|
||||
__LEGACY: { I18nContext },
|
||||
devTools,
|
||||
feature_catalogue,
|
||||
} = pluginSet;
|
||||
|
@ -62,7 +62,6 @@ export class ConsoleUIPlugin implements Plugin<any, any> {
|
|||
boot({
|
||||
docLinkVersion: ctx.core.docLinks.DOC_LINK_VERSION,
|
||||
I18nContext,
|
||||
ResizeChecker,
|
||||
notifications,
|
||||
}),
|
||||
element
|
||||
|
|