diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 36e9f4220b35c..c179dbadac533 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -39,10 +39,6 @@ #CC# /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app #CC# /src/legacy/server/url_shortening/ @elastic/kibana-app #CC# /src/legacy/ui/public/state_management @elastic/kibana-app -#CC# /src/plugins/advanced_settings/ @elastic/kibana-app -#CC# /src/plugins/charts/ @elastic/kibana-app -#CC# /src/plugins/index_pattern_management/public @elastic/kibana-app -#CC# /src/plugins/vis_default_editor @elastic/kibana-app # App Architecture /examples/bfetch_explorer/ @elastic/kibana-app-arch @@ -74,7 +70,7 @@ /x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-arch #CC# /src/plugins/bfetch/ @elastic/kibana-app-arch -#CC# /src/plugins/index_pattern_management/public/service @elastic/kibana-app-arch +#CC# /src/plugins/index_pattern_management/ @elastic/kibana-app-arch #CC# /src/plugins/inspector/ @elastic/kibana-app-arch #CC# /src/plugins/share/ @elastic/kibana-app-arch #CC# /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch diff --git a/docs/api/saved-objects/rotate_encryption_key.asciidoc b/docs/api/saved-objects/rotate_encryption_key.asciidoc index 0a66ed2b4b361..efc57ddb4308d 100644 --- a/docs/api/saved-objects/rotate_encryption_key.asciidoc +++ b/docs/api/saved-objects/rotate_encryption_key.asciidoc @@ -25,7 +25,7 @@ Bulk key rotation can consume a considerable amount of resources and hence only `type`:: (Optional, string) Limits encryption key rotation only to the saved objects with the specified type. By default, {kib} tries to rotate the encryption key for all saved object types that may contain encrypted attributes. -`batchSize`:: +`batch_size`:: (Optional, number) Specifies a maximum number of saved objects that {kib} can process in a single batch. Bulk key rotation is an iterative process since {kib} may not be able to fetch and process all required saved objects in one go and splits processing into consequent batches. By default, the batch size is 10000, which is also a maximum allowed value. [[saved-objects-api-rotate-encryption-key-response-body]] @@ -91,7 +91,7 @@ In this example, key rotation is performed for all saved objects with the `alert [source,sh] -------------------------------------------------- -$ curl -X POST /api/encrypted_saved_objects/_rotate_key?type=alert&batchSize=5000 +$ curl -X POST /api/encrypted_saved_objects/_rotate_key?type=alert&batch_size=5000 -------------------------------------------------- // KIBANA diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 4387e168f412d..d0808d73151b0 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -22,8 +22,10 @@ NOTE: |Description -|{kib-repo}blob/{branch}/src/plugins/advanced_settings[advancedSettings] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/advanced_settings/README.md[advancedSettings] +|This plugin contains the advanced settings management section +allowing users to configure their advanced settings, also known +as uiSettings within the code. |{kib-repo}blob/{branch}/src/plugins/apm_oss[apmOss] @@ -130,8 +132,10 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |WARNING: Missing README. -|{kib-repo}blob/{branch}/src/plugins/management[management] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/management/README.md[management] +|This plugins contains the "Stack Management" page framework. It offers navigation and an API +to link individual managment section into it. This plugin does not contain any individual +management section itself. |{kib-repo}blob/{branch}/src/plugins/maps_legacy/README.md[mapsLegacy] diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md new file mode 100644 index 0000000000000..dfe25c5c9e42d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getBreadcrumbsAppendExtension$](./kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md) + +## ChromeStart.getBreadcrumbsAppendExtension$() method + +Get an observable of the current extension appended to breadcrumbs + +Signature: + +```typescript +getBreadcrumbsAppendExtension$(): Observable; +``` +Returns: + +`Observable` + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index 2594848ef0847..663b326193de5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -55,6 +55,7 @@ core.chrome.setHelpExtension(elem => { | [getBadge$()](./kibana-plugin-core-public.chromestart.getbadge_.md) | Get an observable of the current badge | | [getBrand$()](./kibana-plugin-core-public.chromestart.getbrand_.md) | Get an observable of the current brand information. | | [getBreadcrumbs$()](./kibana-plugin-core-public.chromestart.getbreadcrumbs_.md) | Get an observable of the current list of breadcrumbs | +| [getBreadcrumbsAppendExtension$()](./kibana-plugin-core-public.chromestart.getbreadcrumbsappendextension_.md) | Get an observable of the current extension appended to breadcrumbs | | [getCustomNavLink$()](./kibana-plugin-core-public.chromestart.getcustomnavlink_.md) | Get an observable of the current custom nav link | | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | | [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | @@ -64,6 +65,7 @@ core.chrome.setHelpExtension(elem => { | [setBadge(badge)](./kibana-plugin-core-public.chromestart.setbadge.md) | Override the current badge | | [setBrand(brand)](./kibana-plugin-core-public.chromestart.setbrand.md) | Set the brand configuration. | | [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-core-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs | +| [setBreadcrumbsAppendExtension(breadcrumbsAppendExtension)](./kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md) | Mount an element next to the last breadcrumb | | [setCustomNavLink(newCustomNavLink)](./kibana-plugin-core-public.chromestart.setcustomnavlink.md) | Override the current set of custom nav link | | [setHelpExtension(helpExtension)](./kibana-plugin-core-public.chromestart.sethelpextension.md) | Override the current set of custom help content | | [setHelpSupportUrl(url)](./kibana-plugin-core-public.chromestart.sethelpsupporturl.md) | Override the default support URL shown in the help menu | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md new file mode 100644 index 0000000000000..02adb9b4d325d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [setBreadcrumbsAppendExtension](./kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md) + +## ChromeStart.setBreadcrumbsAppendExtension() method + +Mount an element next to the last breadcrumb + +Signature: + +```typescript +setBreadcrumbsAppendExtension(breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| breadcrumbsAppendExtension | ChromeBreadcrumbsAppendExtension | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index bbf856480aedd..b2f8e83d8e654 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -17,6 +17,6 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | -| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management | +| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md index 7f39d9714a3a3..739fdfdeb5fc3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md @@ -4,7 +4,7 @@ ## ISearchSetup.session property -session management +session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index 4a69e94dd6f58..dba60c7bdf147 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,6 +19,6 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | -| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management | +| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md index de25cccd6d27a..1ad194a9bec86 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md @@ -4,7 +4,7 @@ ## ISearchStart.session property -session management +session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md new file mode 100644 index 0000000000000..fc3d214eb4cad --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) + +## ISessionService.clear property + +Clears the active session. + +Signature: + +```typescript +clear: () => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md new file mode 100644 index 0000000000000..e30c89fb1a9fd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) + +## ISessionService.getSession$ property + +Returns the observable that emits an update every time the session ID changes + +Signature: + +```typescript +getSession$: () => Observable; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md new file mode 100644 index 0000000000000..838023ff1d8b9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) + +## ISessionService.getSessionId property + +Returns the active session ID + +Signature: + +```typescript +getSessionId: () => string | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md new file mode 100644 index 0000000000000..174f9dbe66bf4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) + +## ISessionService interface + +Signature: + +```typescript +export interface ISessionService +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) | () => void | Clears the active session. | +| [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) | () => Observable<string | undefined> | Returns the observable that emits an update every time the session ID changes | +| [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) | () => string | undefined | Returns the active session ID | +| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => void | Restores existing session | +| [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) | () => string | Starts a new session | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md new file mode 100644 index 0000000000000..857e85bbd30eb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) + +## ISessionService.restore property + +Restores existing session + +Signature: + +```typescript +restore: (sessionId: string) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md new file mode 100644 index 0000000000000..9e14c5ed26765 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) + +## ISessionService.start property + +Starts a new session + +Signature: + +```typescript +start: () => string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 6a3c437305cc8..ac6923fd12f96 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -74,6 +74,7 @@ | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | | [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | search service | | [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service | +| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md new file mode 100644 index 0000000000000..62610624655a1 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) + +## isContextMenuTriggerContext variable + +Signature: + +```typescript +isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md index df67eda5074b9..06f792837e4fe 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md @@ -77,6 +77,7 @@ | [contextMenuTrigger](./kibana-plugin-plugins-embeddable-public.contextmenutrigger.md) | | | [defaultEmbeddableFactoryProvider](./kibana-plugin-plugins-embeddable-public.defaultembeddablefactoryprovider.md) | | | [EmbeddableRenderer](./kibana-plugin-plugins-embeddable-public.embeddablerenderer.md) | Helper react component to render an embeddable Can be used if you have an embeddable object or an embeddable factory Supports updating input by passing input prop | +| [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) | | | [isRangeSelectTriggerContext](./kibana-plugin-plugins-embeddable-public.israngeselecttriggercontext.md) | | | [isValueClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isvalueclicktriggercontext.md) | | | [PANEL\_BADGE\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_badge_trigger.md) | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md new file mode 100644 index 0000000000000..cce7a463d1561 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md) > [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md) + +## ExpressionFunctionDefinitions.derivative property + +Signature: + +```typescript +derivative: ExpressionFunctionDerivative; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md index d1703a1e019e6..0e180d1fabe39 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md @@ -18,6 +18,7 @@ export interface ExpressionFunctionDefinitions | --- | --- | --- | | [clog](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.clog.md) | ExpressionFunctionClog | | | [cumulative\_sum](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md) | ExpressionFunctionCumulativeSum | | +| [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative | | | [font](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont | | | [kibana\_context](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md) | ExpressionFunctionKibanaContext | | | [kibana](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md) | ExpressionFunctionKibana | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md new file mode 100644 index 0000000000000..6c51f1eb97750 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) > [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md) + +## ExpressionFunctionDefinitions.derivative property + +Signature: + +```typescript +derivative: ExpressionFunctionDerivative; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md index 05b4ddce4ccde..d4b71a36e0de1 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md @@ -18,6 +18,7 @@ export interface ExpressionFunctionDefinitions | --- | --- | --- | | [clog](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.clog.md) | ExpressionFunctionClog | | | [cumulative\_sum](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md) | ExpressionFunctionCumulativeSum | | +| [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative | | | [font](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont | | | [kibana\_context](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md) | ExpressionFunctionKibanaContext | | | [kibana](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md) | ExpressionFunctionKibana | | diff --git a/docs/management/images/management-index-templates-mappings.png b/docs/management/images/management-index-templates-mappings.png deleted file mode 100644 index beb964b348171..0000000000000 Binary files a/docs/management/images/management-index-templates-mappings.png and /dev/null differ diff --git a/docs/management/images/management-index-templates.png b/docs/management/images/management-index-templates.png deleted file mode 100644 index 07f1fb9a7add1..0000000000000 Binary files a/docs/management/images/management-index-templates.png and /dev/null differ diff --git a/docs/management/images/management_index_component_template.png b/docs/management/images/management_index_component_template.png deleted file mode 100644 index c03029fd172f0..0000000000000 Binary files a/docs/management/images/management_index_component_template.png and /dev/null differ diff --git a/docs/management/images/management_index_create_wizard.png b/docs/management/images/management_index_create_wizard.png deleted file mode 100644 index bff1dd4cd0e7a..0000000000000 Binary files a/docs/management/images/management_index_create_wizard.png and /dev/null differ diff --git a/docs/management/images/management_index_data_stream_backing_index.png b/docs/management/images/management_index_data_stream_backing_index.png deleted file mode 100644 index a5c577affbbb2..0000000000000 Binary files a/docs/management/images/management_index_data_stream_backing_index.png and /dev/null differ diff --git a/docs/management/images/management_index_data_stream_stats.png b/docs/management/images/management_index_data_stream_stats.png deleted file mode 100644 index a67ab4a7deb32..0000000000000 Binary files a/docs/management/images/management_index_data_stream_stats.png and /dev/null differ diff --git a/docs/management/images/management_index_details.png b/docs/management/images/management_index_details.png deleted file mode 100644 index b199d13218f5a..0000000000000 Binary files a/docs/management/images/management_index_details.png and /dev/null differ diff --git a/docs/management/images/management_index_labels.png b/docs/management/images/management_index_labels.png deleted file mode 100644 index a89c32e08beff..0000000000000 Binary files a/docs/management/images/management_index_labels.png and /dev/null differ diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc deleted file mode 100644 index 8416c164c6c51..0000000000000 --- a/docs/management/managing-indices.asciidoc +++ /dev/null @@ -1,258 +0,0 @@ -[role="xpack"] -[[managing-indices]] -== Index Management - -*Index Management* features are an easy, convenient way to manage your -{es} cluster's indices, data streams, and index templates. Practicing good index -management ensures your data is stored correctly and in the most cost-effective -way possible. - -[float] -=== What you'll learn - -This page shows you how to use *Index Management* features to: - -To manage your indices, open the main menu, then click *Stack Management > Index Management*. - -[role="screenshot"] -image::images/management_index_labels.png[Index Management UI] - -[float] -=== Before you start - -Before using this feature, you should be familiar with index management -operations. Refer to the {ref}/indices.html[index management APIs], the -{ref}/indices-templates.html[index template APIs], and the -{ref}/data-streams.html[data streams documentation]. - -[float] -=== Required permissions - -The minimum required permissions to access *Index Management* are -the `monitor` cluster privilege and the `view_index_metadata` -and `manage` index privileges to view the data. -For index templates, you must have the `manage_index_templates` cluster privilege. -See {ref}/security-privileges.html[Security privileges] for more -information. - -You can add these privileges in *Stack Management > Security > Roles*. - -[float] -=== View and edit indices - -When you open *Index Management*, you’re presented an overview of your configured indices. -Badges indicate if an index is {ref}/frozen-indices.html[frozen], -a {ref}/ccr-put-follow.html[follower index], -or a {ref}/rollup-get-rollup-index-caps.html[rollup index]. - -Clicking a badge narrows the list to only indices of that type. -You can also filter your indices using the search bar. - -You can drill down into each index to investigate the index -{ref}/index-modules.html#index-modules-settings[settings], {ref}/mapping.html[mapping], and statistics. -From this view, you can also edit the index settings. - -[role="screenshot"] -image::images/management_index_details.png[Index Management UI] - -[float] -=== Perform index-level operations - -Use the *Manage* menu to perform index-level operations. This menu -is available in the index details view, or when you select the checkbox of one or more -indices on the overview page. The menu includes the following actions: - -* *Close index*. Blocks the index from read/write operations. -A closed index exists in the cluster, but doesn't consume resources -other than disk space. If you reopen a closed index, it goes through the -normal recovery process. - -* *Force merge index*. Reduces the number of segments in your shard by -merging smaller files and clearing deleted ones. Only force merge a read-only index. - -* *Refresh index*. Writes the operations in the indexing buffer to the -filesystem cache. This action is automatically performed once per second. Forcing a manual -refresh is useful during testing, but should not be routinely done in -production because it has a performance impact. - -* *Clear index cache*. Clears all caches associated with the index. - -* *Flush index*. Frees memory by syncing the filesystem cache to disk and -clearing the cache. Once the sync is complete, the internal transaction log is reset. - -* *Freeze index*. Makes the index read-only and reduces its memory footprint -by moving shards to disk. Frozen indices remain -searchable, but queries take longer. - -* *Delete index*. Permanently removes the index and all of its documents. - -* *Add lifecycle policy*. Specifies a policy for managing the lifecycle of the -index. - -[float] -[[manage-data-streams]] -=== Manage data streams - -A {ref}/data-streams.html[data stream] lets you store time series data across -multiple backing indices while giving you a single named resource to use in -requests. The *Data Streams* view lists your data streams and lets you examine -or delete them. - -To view more information about a data stream, such as its generation or its -current index lifecycle policy, click the stream's name. - -[role="screenshot"] -image::images/management_index_data_stream_stats.png[Data stream details] - -To view information about the stream's backing indices, click the number in the -*Indices* column. - -[role="screenshot"] -image::images/management_index_data_stream_backing_index.png[Backing index] - -[float] -[[manage-index-templates]] -=== Manage index templates - -An index template defines {ref}/index-modules.html#index-modules-settings[settings], -{ref}/mapping.html[mappings], and {ref}/indices-add-alias.html[aliases] -that you can automatically apply when creating a new index. {es} applies a -template to a new index based on an index pattern that matches the index name. - -The *Index Templates* view lists your templates and enables you to examine, edit, clone, and -delete them. Changes you make to an index template -do not affect existing indices. - -[role="screenshot"] -image::images/management-index-templates.png[Index templates] - -If you don't have any templates, you can create one using the *Create template* wizard. -Index templates are applied during index creation, -so you must create the -template before you create the indices. - -[float] -==== Try it: Create an index template - -In this tutorial, you’ll create an index template for randomly generated log -files. You'll then use the template to configure two new indices. - -*Step 1. Add a name and index pattern* - -. In the *Index Templates* view, open the *Create template* wizard. -+ -[role="screenshot"] -image::images/management_index_create_wizard.png[Create wizard] - -. In the *Name* field, enter `my-index-template`. - -. Set *Index pattern* to `my-index-*` so the template matches any index -with that index pattern. - -. Leave *Data Stream*, *Priority*, *Version*, and *_meta field* as-is or blank. - -. Click *Next*. - -*Step 2. Add settings, mappings, and index aliases* - -. Add component templates to your index template. -+ -{ref}/indices-component-template.html[Component templates] are pre-configured -sets of mappings, index settings, and index aliases you can reuse across -multiple index templates. Badges indicate whether a component template contains -mappings (*M*), index settings (*S*), index aliases (*A*), or a combination of -the three. -+ -Component templates are optional. For this tutorial, do not add any component -templates. -+ -[role="screenshot"] -image::images/management_index_component_template.png[Component templates page] - -. Define index settings. These are optional. For this tutorial, leave this -section blank. - -. Define a mapping that contains an object field named `geo` with a child -geo-point field named `coordinates`: -+ -[role="screenshot"] -image::images/management-index-templates-mappings.png[Mapped fields page] -+ -Alternatively, you can click the *Load JSON* link and define the mapping as JSON: -+ -[source,js] ----- -{ - "properties": { - "geo": { - "properties": { - "coordinates": { - "type": "geo_point" - } - } - } - } -} ----- -+ -You can create additional mapping configurations in the *Dynamic templates* and -*Advanced options* tabs. No additional mappings are required for this tutorial. - -. Define an index alias named `my-index`: -+ -[source,js] ----- -{ - "my-index": {} -} ----- - -. On the review page, check the summary. If everything looks right, click -*Create template*. - -*Step 3. Create new indices* - -You’re now ready to load the logs data and create new indices using your index -template. - -. In the {kib} *Console*, index the following documents: -+ -[source,js] ----- -POST /my-index-000001/_doc -{ - "@timestamp": "2019-05-18T15:57:27.541Z", - "ip": "225.44.217.191", - "extension": "jpg", - "response": "200", - "geo": { - "coordinates": { - "lat": 38.53146222, - "lon": -121.7864906 - } - }, - "url": "https://media-for-the-masses.theacademyofperformingartsandscience.org/uploads/charles-fullerton.jpg" -} - -POST /my-index-000002/_doc -{ - "@timestamp": "2019-05-20T03:44:20.844Z", - "ip": "198.247.165.49", - "extension": "php", - "response": "200", - "geo": { - "coordinates": { - "lat": 37.13189556, - "lon": -76.4929875 - } - }, - "memory": 241720, - "url": "https://theacademyofperformingartsandscience.org/people/type:astronauts/name:laurel-b-clark/profile" -} ----- -+ -These requests create two indices: `my-index-000001` and `my-index-000002`. - -. Use the {es} {ref}/indices-get-index.html#indices-get-index[get index API] to -view one of the newly created indices. The index's mappings and alias are -configured automatically based on the template. diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index 69c33aa7a1dac..aded7a45022db 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -46,7 +46,7 @@ To manage roles, open then main menu, then click *Stack Management > Roles*, or all users with the same role. NOTE: If you are creating a threshold watch, you must also have the `view_index_metadata` index privilege. See -<> for detailed information. +{ref}/index-mgmt.html[Index management] for detailed information. [float] [[watcher-create-threshold-alert]] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 7fc8fe5114e1e..833273c7b3ef0 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -143,3 +143,8 @@ This content has moved. See This content has moved. See <>. +[role="exclude",id="managing-indices"] +== Index management + +This content has moved. See {ref}/index-mgmt.html[Index management]. + diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index c968ca6f35029..03a728a15351e 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -1,108 +1,73 @@ [[connect-to-elasticsearch]] -== Add data to {kib} +== Add data +++++ +Add data +++++ -To start working with your data in {kib}, you can: +To start working with your data in {kib}, use one of the many ingest options, +available from the home page. +You can collect data from an app or service +or upload a file that contains your data. If you're not ready to use your own data, +add a sample data set and give {kib} a test drive. -* Upload a CSV, JSON, or log file with the File Data Visualizer. - -* Upload geospatial data with the GeoJSON Upload feature. - -* Index logs, metrics, events, or application data by setting up a Beats module. +[role="screenshot"] +image::images/add-data-home.png[Built-in options for adding data to Kibana: Add data, Add Elastic Agent, Upload a file] -* Connect {kib} with existing {es} indices. +[float] +[[add-data-tutorial-kibana]] +=== Add data -If you're not ready to use your own data, you can add a <> -to see all that you can do in {kib}. +Want to ingest logs, metrics, security, or application data? +Install and configure a Beats data shipper or other module to periodically collect the data +and send it to {es}. You can then use the pre-built dashboards to explore and analyze the data. -[float] -[[upload-data-kibana]] -=== Upload a CSV, JSON, or log file +[role="screenshot"] +image::images/add-data-tutorials.png[Add Data tutorials] -experimental[] +[discrete] +=== Add Elastic Agent -To visualize data in a CSV, JSON, or log file, you can upload it using the File -Data Visualizer. On the home page, click *Upload a file*, and -then drag your file onto the *File Data Visualizer*. Alternatively, you can open -it by navigating to *Machine Learning* from the side navigation and selecting +beta[] *Elastic Agent* is a sneak peek at the next generation of +data integration modules, offering +a centralized way to set up your integrations. +With *Fleet*, you can add +and manage integrations for popular services and platforms, providing +an easy way to collect your data. The integrations +ship with dashboards and visualizations, +so you can quickly get insights into your data. -*Data Visualizer*. +To get started, refer to +{ingest-guide}/ingest-management-getting-started.html[Quick start: Get logs and metrics into the Elastic Stack]. [role="screenshot"] -image::images/ingest-data.png[File Data Visualizer on the home page] +image::images/add-data-fleet.png[Add data using Fleet] + +[discrete] +[[upload-data-kibana]] +=== Upload a file -You can upload a file up to 100 MB. This value is configurable up to 1 GB in -<>. +experimental[] If your data is in a CSV, JSON, or log file, you can upload it using the File +Data Visualizer. You can upload a file up to 100 MB. This value is configurable up to 1 GB in +<>. To upload a file with geospatial data, +refer to <>. [role="screenshot"] image::images/add-data-fv.png[File Data Visualizer] -The File Data Visualizer uses the {ref}/ml-find-file-structure.html[find_file_structure API] to analyze -the uploaded file and to suggest ingest pipelines and mappings for your data. + NOTE: This feature is not intended for use as part of a repeated production process, but rather for the initial exploration of your data. -[float] -[[upload-geoipdata-kibana]] -=== Upload geospatial data - -To visualize geospatial data in a point or shape file, you can upload it using the <> -feature in Maps, and then use that data as a layer in a map. -The data is also available for use in the broader Kibana ecosystem, for example, -in visualizations and Canvas workpads. -With GeoJSON Upload, you can upload a file up to 50 MB. - -[float] -[[add-data-tutorial-kibana]] -=== Index metrics, log, security, and application data -The built-in data tutorials can help you quickly get up and running with -metrics data, log analytics, security events, and application data. -These tutorials walk you through installing and configuring a -Beats data shipper to periodically collect and send data to {es}. -You can then use the pre-built dashboards to explore and analyze the data. +[discrete] +=== Additional options for loading your data -You access the tutorials from the home page. -If a tutorial doesn’t exist for your data, go to the {beats-ref}/beats-reference.html[Beats overview] -to learn about other data shippers in the Beats family. +If the {kib} ingest options don't work for you, you can index your +data into Elasticsearch with {ref}/getting-started-index.html[REST APIs] +or https://www.elastic.co/guide/en/elasticsearch/client/index.html[client libraries]. +After you add your data, you're required to create an <> to tell +{kib} where to find the data. -[role="screenshot"] -image::images/add-data-tutorials.png[Add Data tutorials] - - -[float] -[[connect-to-es]] -=== Connect with {es} indices - -To visualize data in existing {es} indices, you must -create an index pattern that matches the names of the indices that you want to explore. -When you add data with the File Data Visualizer, GeoJSON Upload feature, -or built-in tutorial, an index pattern is created for you. - -. Open the main menu, then click *Stack Management > Index Patterns*. - -. Click *Create index pattern*. - -. Specify an index pattern that matches the name of one or more of your Elasticsearch indices. -+ -For example, an index pattern can point to your Apache data from yesterday, -`filebeat-apache-4-3-2022`, or any index that matches the pattern, `filebeat-*`. -Using a wildcard is the more popular approach. - - -. Click *Next Step*, and then select the index field that contains the timestamp you want to use to perform time-based -comparisons. -+ -Kibana reads the index mapping and lists all fields that contain a timestamp. If your -index doesn't have time-based data, choose *I don't want to use the time filter*. -+ -You must select a time field to use global time filters on your dashboards. - -. Click *Create index pattern*. -+ -{kib} is now configured to access your {es} indices. -You’ll see a list of fields configured for the matching index. -You can designate your index pattern as the default by clicking the star icon on this page. -+ -When searching in *Discover* and creating visualizations, you choose a pattern -from the index pattern menu to specify the {es} indices that contain the data you want to explore. +* To add data for Elastic Observability, refer to {observability-guide}/add-observability-data.html[Send data to Elasticsearch]. +* To add data for Elastic Security, refer to https://www.elastic.co/guide/en/security/current/ingest-data.html[Ingest data to Elastic Security]. diff --git a/docs/setup/images/add-data-fleet.png b/docs/setup/images/add-data-fleet.png new file mode 100644 index 0000000000000..b6d49cfaf8d3a Binary files /dev/null and b/docs/setup/images/add-data-fleet.png differ diff --git a/docs/setup/images/add-data-home-page.png b/docs/setup/images/add-data-home-page.png new file mode 100644 index 0000000000000..a3373757683be Binary files /dev/null and b/docs/setup/images/add-data-home-page.png differ diff --git a/docs/setup/images/add-data-home.png b/docs/setup/images/add-data-home.png new file mode 100644 index 0000000000000..3a844b1c40de9 Binary files /dev/null and b/docs/setup/images/add-data-home.png differ diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 91f149d5cdb3c..9d580187edff4 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -26,30 +26,16 @@ image::images/intro-kibana.png[Kibana home page] [[get-data-into-kibana]] === Ingest data -{kib} is designed to use {es} as a data source. Think of {es} as the engine that stores +{kib} is designed to use {es} as a data source. Think of Elasticsearch as the engine that stores and processes the data, with {kib} sitting on top. -From the home page, {kib} provides these options for ingesting data: - -* Import data using the -https://www.elastic.co/blog/importing-csv-and-log-data-into-elasticsearch-with-file-data-visualizer[File Data visualizer]. -* Set up a data flow to Elasticsearch using our built-in tutorials. -If a tutorial doesn’t exist for your data, go to the -{beats-ref}/beats-reference.html[Beats overview] to learn about other data shippers -in the {beats} family. -* <> and take {kib} for a test drive without loading data yourself. -* Index your data into Elasticsearch with {ref}/getting-started-index.html[REST APIs] - or https://www.elastic.co/guide/en/elasticsearch/client/index.html[client libraries]. -+ -[role="screenshot"] -image::images/intro-data-tutorial.png[Ways to get data in from the home page] - +To start working with your data in Kibana, use one of the many ingest options, +available from the home page. You can collect data from an app or service or upload a file that contains your data. +If you're not ready to use your own data, you can add a sample data set +to give {kib} a test drive. -{kib} uses an -<> to tell it which {es} indices to explore. -If you add upload a file, run a built-in tutorial, or add sample data, you get an index pattern for free, -and are good to start exploring. If you load your own data, you can create -an index pattern in <>. +[role="screenshot"] +image::setup/images/add-data-home.png[Built-in options for adding data to Kibana: Add data, Add Elastic Agent, Upload a file] [float] [[explore-and-query]] @@ -94,7 +80,7 @@ and dynamic client-side styling. * <> allows you to combine an infinite number of aggregations to display complex data. -With TSVB, you can analyze multiple index patterns and customize +With TSVB, you can customize every aspect of your visualization. Choose your own date format and color gradients, and easily switch your data view between time series, metric, top N, gauge, and markdown. @@ -124,7 +110,7 @@ dashboards in one space, but full access to all of Kibana’s features in anothe === Manage all things Elastic Stack <> provides guided processes for managing all -things Elastic Stack — indices, clusters, licenses, UI settings, index patterns, +things Elastic Stack — indices, clusters, licenses, UI settings, and more. Want to update your {es} indices? Set user roles and privileges? Turn on dark mode? Kibana has UIs for all that. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index ee85819b4fd98..503f97aabd13f 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -39,7 +39,7 @@ quickly deploy configuration changes to all Beats running across your enterprise [cols="50, 50"] |=== -a| <> +a| {ref}/index-mgmt.html[Index Management] | View index settings, mappings, and statistics and perform operations, such as refreshing, flushing, and clearing the cache. Practicing good index management ensures that your data is stored cost effectively. @@ -184,8 +184,6 @@ include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[] include::{kib-repo-dir}/management/managing-beats.asciidoc[] -include::{kib-repo-dir}/management/managing-indices.asciidoc[] - include::{kib-repo-dir}/management/ingest-pipelines/ingest-pipelines.asciidoc[] include::{kib-repo-dir}/management/managing-fields.asciidoc[] diff --git a/docs/user/monitoring/beats-details.asciidoc b/docs/user/monitoring/beats-details.asciidoc index 3d7a726d2f8a2..dbd9aecd04768 100644 --- a/docs/user/monitoring/beats-details.asciidoc +++ b/docs/user/monitoring/beats-details.asciidoc @@ -9,7 +9,7 @@ If you are monitoring Beats, the *Stack Monitoring* page in {kib} contains a panel for Beats in the cluster overview. [role="screenshot"] -image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="images/monitoring-beats.jpg"] +image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="user/monitoring/images/monitoring-beats.jpg"] To view an overview of the Beats data in the cluster, click *Overview*. The overview page has a section for activity in the last day, which is a real-time @@ -24,7 +24,7 @@ cluster. All columns are sortable. Clicking a Beat name takes you to the detail page. For example: [role="screenshot"] -image::user/monitoring/images/monitoring-beats-detail.jpg["Monitoring details for Filebeat",link="images/monitoring-beats-detail.jpg"] +image::user/monitoring/images/monitoring-beats-detail.jpg["Monitoring details for Filebeat",link="user/monitoring/images/monitoring-beats-detail.jpg"] The detail page contains a summary bar and charts. There are more charts on this page than the overview page and they are specific to a single Beat instance. diff --git a/package.json b/package.json index 3a2d13fd5ef3b..1d5c5059cd03e 100644 --- a/package.json +++ b/package.json @@ -110,14 +110,16 @@ "**/grunt-*/**", "x-pack/typescript", "@elastic/eui/rehype-react", + "@elastic/eui/remark-parse", "@elastic/eui/remark-rehype", - "@elastic/eui/remark-rehype/**" + "@elastic/eui/remark-rehype/**", + "@elastic/eui/unified" ] }, "dependencies": { "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "7.10.0-rc.1", - "@elastic/eui": "29.5.0", + "@elastic/eui": "30.1.1", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 3f9fdb164e759..770bb4f510301 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -54,7 +54,7 @@ pageLoadAssetSize: mapsLegacy: 116817 mapsLegacyLicensing: 20214 ml: 82187 - monitoring: 268612 + monitoring: 50000 navigation: 37269 newsfeed: 42228 observability: 89709 diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index d954ae0823caf..b47adb29f1e68 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@babel/core": "^7.11.6", - "@elastic/eui": "29.5.0", + "@elastic/eui": "30.1.1", "@kbn/babel-preset": "1.0.0", "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index b1b5d6e2b419e..e6883be307bb2 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@elastic/charts": "24.0.0", - "@elastic/eui": "29.5.0", + "@elastic/eui": "30.1.1", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", "@kbn/monaco": "1.0.0", diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index cbcd23615d34c..6df8d57c8c574 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -63,6 +63,8 @@ const createStartContractMock = () => { setBadge: jest.fn(), getBreadcrumbs$: jest.fn(), setBreadcrumbs: jest.fn(), + getBreadcrumbsAppendExtension$: jest.fn(), + setBreadcrumbsAppendExtension: jest.fn(), getHelpExtension$: jest.fn(), setHelpExtension: jest.fn(), setHelpSupportUrl: jest.fn(), @@ -76,6 +78,7 @@ const createStartContractMock = () => { startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name'])); startContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge)); startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])); + startContract.getBreadcrumbsAppendExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getCustomNavLink$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 0150554a60906..3783f3bd9b65e 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -363,6 +363,25 @@ describe('start', () => { }); }); + describe('breadcrumbsAppendExtension$', () => { + it('updates the breadcrumbsAppendExtension$', async () => { + const { chrome, service } = await start(); + const promise = chrome.getBreadcrumbsAppendExtension$().pipe(toArray()).toPromise(); + + chrome.setBreadcrumbsAppendExtension({ content: (element) => () => {} }); + service.stop(); + + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + undefined, + Object { + "content": [Function], + }, + ] + `); + }); + }); + describe('custom nav link', () => { it('updates/emits the current custom nav link', async () => { const { chrome, service } = await start(); @@ -429,33 +448,33 @@ describe('start', () => { expect(docTitleResetSpy).toBeCalledTimes(1); await expect(promises).resolves.toMatchInlineSnapshot(` - Array [ - Array [ - undefined, - Object { - "appName": "App name", - }, - undefined, - ], - Array [ - Array [], - Array [ - Object { - "text": "App breadcrumb", - }, - ], - Array [], - ], - Array [ - undefined, - Object { - "text": "App badge", - "tooltip": "App tooltip", - }, - undefined, - ], - ] - `); + Array [ + Array [ + undefined, + Object { + "appName": "App name", + }, + undefined, + ], + Array [ + Array [], + Array [ + Object { + "text": "App breadcrumb", + }, + ], + Array [], + ], + Array [ + undefined, + Object { + "text": "App badge", + "tooltip": "App tooltip", + }, + undefined, + ], + ] + `); }); }); }); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index b01f120b81305..b39c83498859c 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -24,6 +24,7 @@ import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } import { flatMap, map, takeUntil } from 'rxjs/operators'; import { parse } from 'url'; import { EuiLink } from '@elastic/eui'; +import { MountPoint } from '../types'; import { mountReactNode } from '../utils/mount'; import { InternalApplicationStart } from '../application'; import { DocLinksStart } from '../doc_links'; @@ -58,6 +59,11 @@ export interface ChromeBrand { /** @public */ export type ChromeBreadcrumb = EuiBreadcrumb; +/** @public */ +export interface ChromeBreadcrumbsAppendExtension { + content: MountPoint; +} + /** @public */ export interface ChromeHelpExtension { /** @@ -146,6 +152,9 @@ export class ChromeService { const applicationClasses$ = new BehaviorSubject>(new Set()); const helpExtension$ = new BehaviorSubject(undefined); const breadcrumbs$ = new BehaviorSubject([]); + const breadcrumbsAppendExtension$ = new BehaviorSubject< + ChromeBreadcrumbsAppendExtension | undefined + >(undefined); const badge$ = new BehaviorSubject(undefined); const customNavLink$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); @@ -225,6 +234,7 @@ export class ChromeService { badge$={badge$.pipe(takeUntil(this.stop$))} basePath={http.basePath} breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))} + breadcrumbsAppendExtension$={breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$))} customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))} kibanaDocLink={docLinks.links.kibana} forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()} @@ -290,6 +300,14 @@ export class ChromeService { breadcrumbs$.next(newBreadcrumbs); }, + getBreadcrumbsAppendExtension$: () => breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$)), + + setBreadcrumbsAppendExtension: ( + breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension + ) => { + breadcrumbsAppendExtension$.next(breadcrumbsAppendExtension); + }, + getHelpExtension$: () => helpExtension$.pipe(takeUntil(this.stop$)), setHelpExtension: (helpExtension?: ChromeHelpExtension) => { @@ -431,6 +449,18 @@ export interface ChromeStart { */ setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + /** + * Get an observable of the current extension appended to breadcrumbs + */ + getBreadcrumbsAppendExtension$(): Observable; + + /** + * Mount an element next to the last breadcrumb + */ + setBreadcrumbsAppendExtension( + breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension + ): void; + /** * Get an observable of the current custom nav link */ diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 34c5f213a7221..ee2fcbd5078af 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -300,6 +300,55 @@ exports[`Header renders 1`] = ` "thrownError": null, } } + breadcrumbsAppendExtension$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -5029,6 +5078,55 @@ exports[`Header renders 1`] = ` "thrownError": null, } } + breadcrumbsAppendExtension$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } > ; badge$: Observable; breadcrumbs$: Observable; + breadcrumbsAppendExtension$: Observable; customNavLink$: Observable; homeHref: string; isVisible$: Observable; @@ -169,6 +170,7 @@ export function Header({ diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index 7fe2c91087090..64401171d142a 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -22,12 +22,17 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { BehaviorSubject } from 'rxjs'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; +import { ChromeBreadcrumbsAppendExtension } from '../../chrome_service'; describe('HeaderBreadcrumbs', () => { it('renders updates to the breadcrumbs$ observable', () => { const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); const wrapper = mount( - + ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); @@ -39,4 +44,29 @@ describe('HeaderBreadcrumbs', () => { wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); }); + + it('renders breadcrumbs extension', () => { + const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); + const breadcrumbsAppendExtension$ = new BehaviorSubject< + undefined | ChromeBreadcrumbsAppendExtension + >({ + content: (root: HTMLDivElement) => { + root.innerHTML = '
__render__
'; + return () => (root.innerHTML = ''); + }, + }); + + const wrapper = mount( + + ); + + expect(wrapper.find('.euiBreadcrumb').getDOMNode().querySelector('my-extension')).toBeDefined(); + act(() => breadcrumbsAppendExtension$.next(undefined)); + wrapper.update(); + expect(wrapper.find('.euiBreadcrumb').getDOMNode().querySelector('my-extension')).toBeNull(); + }); }); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index 52412f8990c7a..d52faa87cfecd 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -22,16 +22,19 @@ import classNames from 'classnames'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; -import { ChromeBreadcrumb } from '../../chrome_service'; +import { ChromeBreadcrumb, ChromeBreadcrumbsAppendExtension } from '../../chrome_service'; +import { HeaderExtension } from './header_extension'; interface Props { appTitle$: Observable; breadcrumbs$: Observable; + breadcrumbsAppendExtension$: Observable; } -export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { +export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$, breadcrumbsAppendExtension$ }: Props) { const appTitle = useObservable(appTitle$, 'Kibana'); const breadcrumbs = useObservable(breadcrumbs$, []); + const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$); let crumbs = breadcrumbs; if (breadcrumbs.length === 0 && appTitle) { @@ -48,5 +51,15 @@ export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { ), })); + if (breadcrumbsAppendExtension) { + const lastCrumb = crumbs[crumbs.length - 1]; + lastCrumb.text = ( + <> + {lastCrumb.text} + + + ); + } + return ; } diff --git a/src/core/public/chrome/ui/header/header_extension.test.tsx b/src/core/public/chrome/ui/header/header_extension.test.tsx index 3d5678b8bb7ef..ba00c74b81cfa 100644 --- a/src/core/public/chrome/ui/header/header_extension.test.tsx +++ b/src/core/public/chrome/ui/header/header_extension.test.tsx @@ -32,6 +32,14 @@ describe('HeaderExtension', () => { expect(divNode).toBeInstanceOf(HTMLElement); }); + it('calls navControl.render with div node as inlineBlock', () => { + const renderSpy = jest.fn(); + mount(); + + const [divNode] = renderSpy.mock.calls[0]; + expect(divNode).toHaveAttribute('style', 'display: inline-block;'); + }); + it('calls unrender callback when unmounted', () => { const unrenderSpy = jest.fn(); const render = () => unrenderSpy; diff --git a/src/core/public/chrome/ui/header/header_extension.tsx b/src/core/public/chrome/ui/header/header_extension.tsx index 76413a0ea0317..97cf38f44c3f1 100644 --- a/src/core/public/chrome/ui/header/header_extension.tsx +++ b/src/core/public/chrome/ui/header/header_extension.tsx @@ -22,6 +22,7 @@ import { MountPoint } from '../../../types'; interface Props { extension?: MountPoint; + display?: 'block' | 'inlineBlock'; } export class HeaderExtension extends React.Component { @@ -46,7 +47,12 @@ export class HeaderExtension extends React.Component { } public render() { - return
; + return ( +
+ ); } private renderExtension() { diff --git a/src/core/public/chrome/ui/header/recent_links.tsx b/src/core/public/chrome/ui/header/recent_links.tsx deleted file mode 100644 index 0e068a3465b0e..0000000000000 --- a/src/core/public/chrome/ui/header/recent_links.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiNavDrawerGroup } from '@elastic/eui'; -import { RecentNavLink } from './nav_link'; - -interface Props { - recentNavLinks: RecentNavLink[]; -} - -export function RecentLinks({ recentNavLinks }: Props) { - return ( - - ); -} diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index f52d5b6fbd6a5..4babec38a936e 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -343,6 +343,8 @@ export interface ChromeStart { getBadge$(): Observable; getBrand$(): Observable; getBreadcrumbs$(): Observable; + // Warning: (ae-forgotten-export) The symbol "ChromeBreadcrumbsAppendExtension" needs to be exported by the entry point index.d.ts + getBreadcrumbsAppendExtension$(): Observable; getCustomNavLink$(): Observable | undefined>; getHelpExtension$(): Observable; getIsNavDrawerLocked$(): Observable; @@ -355,6 +357,7 @@ export interface ChromeStart { setBadge(badge?: ChromeBadge): void; setBrand(brand: ChromeBrand): void; setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + setBreadcrumbsAppendExtension(breadcrumbsAppendExtension?: ChromeBreadcrumbsAppendExtension): void; setCustomNavLink(newCustomNavLink?: Partial): void; setHelpExtension(helpExtension?: ChromeHelpExtension): void; setHelpSupportUrl(url: string): void; diff --git a/src/plugins/advanced_settings/README.md b/src/plugins/advanced_settings/README.md new file mode 100644 index 0000000000000..a364348a8e99a --- /dev/null +++ b/src/plugins/advanced_settings/README.md @@ -0,0 +1,5 @@ +# Advanced Settings + +This plugin contains the advanced settings management section +allowing users to configure their advanced settings, also known +as uiSettings within the code. \ No newline at end of file diff --git a/src/plugins/charts/public/static/components/number_input.tsx b/src/plugins/charts/public/static/components/number_input.tsx index 8c2874f522902..68e292861c32e 100644 --- a/src/plugins/charts/public/static/components/number_input.tsx +++ b/src/plugins/charts/public/static/components/number_input.tsx @@ -54,7 +54,7 @@ function NumberInputOption({ 'data-test-subj': dataTestSubj, }: NumberInputOptionProps) { return ( - + ({ } }; return ( - + ({ ); return ( - + ({ setValue, }: SwitchOptionProps) { return ( - + ({ setValue, }: TextInputOptionProps) { return ( - + { return { clear: jest.fn(), start: jest.fn(), + restore: jest.fn(), getSessionId: jest.fn(), getSession$: jest.fn(), }; diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts index 80ab74f1aa14d..6660b8395547f 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/common/search/session/types.ts @@ -34,6 +34,12 @@ export interface ISessionService { * Starts a new session */ start: () => string; + + /** + * Restores existing session + */ + restore: (sessionId: string) => void; + /** * Clears the active session. */ diff --git a/src/plugins/data/common/utils/abort_utils.test.ts b/src/plugins/data/common/utils/abort_utils.test.ts index ca187e95f528b..358f00e5e82bd 100644 --- a/src/plugins/data/common/utils/abort_utils.test.ts +++ b/src/plugins/data/common/utils/abort_utils.test.ts @@ -41,7 +41,7 @@ describe('AbortUtils', () => { describe('rejects', () => { test('should not reject if the signal does not abort', async () => { const controller = new AbortController(); - const promise = toPromise(controller.signal); + const promise = toPromise(controller.signal).promise; const whenRejected = jest.fn(); promise.catch(whenRejected); await flushPromises(); @@ -50,7 +50,7 @@ describe('AbortUtils', () => { test('should reject if the signal does abort', async () => { const controller = new AbortController(); - const promise = toPromise(controller.signal); + const promise = toPromise(controller.signal).promise; const whenRejected = jest.fn(); promise.catch(whenRejected); controller.abort(); @@ -58,13 +58,30 @@ describe('AbortUtils', () => { expect(whenRejected).toBeCalled(); expect(whenRejected.mock.calls[0][0]).toBeInstanceOf(AbortError); }); + + test('should expose cleanup handler', () => { + const controller = new AbortController(); + const promise = toPromise(controller.signal); + expect(promise.cleanup).toBeDefined(); + }); + + test('calling clean up handler prevents rejects', async () => { + const controller = new AbortController(); + const { promise, cleanup } = toPromise(controller.signal); + const whenRejected = jest.fn(); + promise.catch(whenRejected); + cleanup(); + controller.abort(); + await flushPromises(); + expect(whenRejected).not.toBeCalled(); + }); }); }); describe('getCombinedSignal', () => { test('should return an AbortSignal', () => { - const signal = getCombinedSignal([]); - expect(signal instanceof AbortSignal).toBe(true); + const signal = getCombinedSignal([]).signal; + expect(signal).toBeInstanceOf(AbortSignal); }); test('should not abort if none of the signals abort', async () => { @@ -72,7 +89,7 @@ describe('AbortUtils', () => { const controller2 = new AbortController(); setTimeout(() => controller1.abort(), 2000); setTimeout(() => controller2.abort(), 1000); - const signal = getCombinedSignal([controller1.signal, controller2.signal]); + const signal = getCombinedSignal([controller1.signal, controller2.signal]).signal; expect(signal.aborted).toBe(false); jest.advanceTimersByTime(500); await flushPromises(); @@ -84,7 +101,7 @@ describe('AbortUtils', () => { const controller2 = new AbortController(); setTimeout(() => controller1.abort(), 2000); setTimeout(() => controller2.abort(), 1000); - const signal = getCombinedSignal([controller1.signal, controller2.signal]); + const signal = getCombinedSignal([controller1.signal, controller2.signal]).signal; expect(signal.aborted).toBe(false); jest.advanceTimersByTime(1000); await flushPromises(); @@ -95,8 +112,56 @@ describe('AbortUtils', () => { const controller1 = new AbortController(); const controller2 = new AbortController(); controller1.abort(); - const signal = getCombinedSignal([controller1.signal, controller2.signal]); + const signal = getCombinedSignal([controller1.signal, controller2.signal]).signal; expect(signal.aborted).toBe(true); }); + + describe('cleanup listener', () => { + const createMockController = () => { + const controller = new AbortController(); + const spyAddListener = jest.spyOn(controller.signal, 'addEventListener'); + const spyRemoveListener = jest.spyOn(controller.signal, 'removeEventListener'); + return { + controller, + getTotalListeners: () => + Math.max(spyAddListener.mock.calls.length - spyRemoveListener.mock.calls.length, 0), + }; + }; + + test('cleanup should cleanup inner listeners', () => { + const controller1 = createMockController(); + const controller2 = createMockController(); + + const { cleanup } = getCombinedSignal([ + controller1.controller.signal, + controller2.controller.signal, + ]); + + expect(controller1.getTotalListeners()).toBe(1); + expect(controller2.getTotalListeners()).toBe(1); + + cleanup(); + + expect(controller1.getTotalListeners()).toBe(0); + expect(controller2.getTotalListeners()).toBe(0); + }); + + test('abort should cleanup inner listeners', async () => { + const controller1 = createMockController(); + const controller2 = createMockController(); + + getCombinedSignal([controller1.controller.signal, controller2.controller.signal]); + + expect(controller1.getTotalListeners()).toBe(1); + expect(controller2.getTotalListeners()).toBe(1); + + controller1.controller.abort(); + + await flushPromises(); + + expect(controller1.getTotalListeners()).toBe(0); + expect(controller2.getTotalListeners()).toBe(0); + }); + }); }); }); diff --git a/src/plugins/data/common/utils/abort_utils.ts b/src/plugins/data/common/utils/abort_utils.ts index a26fec9423f83..81f30b7454c7b 100644 --- a/src/plugins/data/common/utils/abort_utils.ts +++ b/src/plugins/data/common/utils/abort_utils.ts @@ -36,15 +36,23 @@ export class AbortError extends Error { * * @param signal The `AbortSignal` to generate the `Promise` from */ -export function toPromise(signal: AbortSignal): Promise { - return new Promise((resolve, reject) => { - if (signal.aborted) reject(new AbortError()); - const abortHandler = () => { +export function toPromise(signal: AbortSignal): { promise: Promise; cleanup: () => void } { + let abortHandler: () => void; + const cleanup = () => { + if (abortHandler) { signal.removeEventListener('abort', abortHandler); + } + }; + const promise = new Promise((resolve, reject) => { + if (signal.aborted) reject(new AbortError()); + abortHandler = () => { + cleanup(); reject(new AbortError()); }; signal.addEventListener('abort', abortHandler); }); + + return { promise, cleanup }; } /** @@ -52,13 +60,26 @@ export function toPromise(signal: AbortSignal): Promise { * * @param signals */ -export function getCombinedSignal(signals: AbortSignal[]) { +export function getCombinedSignal( + signals: AbortSignal[] +): { signal: AbortSignal; cleanup: () => void } { const controller = new AbortController(); + let cleanup = () => {}; + if (signals.some((signal) => signal.aborted)) { controller.abort(); } else { const promises = signals.map((signal) => toPromise(signal)); - Promise.race(promises).catch(() => controller.abort()); + cleanup = () => { + promises.forEach((p) => p.cleanup()); + controller.signal.removeEventListener('abort', cleanup); + }; + controller.signal.addEventListener('abort', cleanup); + Promise.race(promises.map((p) => p.promise)).catch(() => { + cleanup(); + controller.abort(); + }); } - return controller.signal; + + return { signal: controller.signal, cleanup }; } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index c041511745be2..c54cb36142cbd 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -380,7 +380,7 @@ export { PainlessError, } from './search'; -export type { SearchSource } from './search'; +export type { SearchSource, ISessionService } from './search'; export { ISearchOptions, isErrorResponse, isCompleteResponse, isPartialResponse } from '../common'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 7ee21236c1c79..b072407a5fe10 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1415,8 +1415,6 @@ export interface ISearchSetup { // // (undocumented) aggs: AggsSetup; - // Warning: (ae-forgotten-export) The symbol "ISessionService" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ISessionService" session: ISessionService; // Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts // @@ -1432,7 +1430,6 @@ export interface ISearchStart { aggs: AggsStart; search: ISearchGeneric; searchSource: ISearchStartSearchSource; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ISessionService" session: ISessionService; // (undocumented) showError: (e: Error) => void; @@ -1449,6 +1446,17 @@ export interface ISearchStartSearchSource { // @public (undocumented) export const isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean | undefined; +// Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ISessionService { + clear: () => void; + getSession$: () => Observable; + getSessionId: () => string | undefined; + restore: (sessionId: string) => void; + start: () => string; +} + // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 86804a819cb0e..1abf3192a4846 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -41,6 +41,7 @@ export { SearchSourceDependencies, SearchSourceFields, SortDirection, + ISessionService, } from '../../common/search'; export { getEsPreference } from './es_search'; diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 087ca9e4f5c47..0f069d54ee9c8 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -24,13 +24,13 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { - getCombinedSignal, AbortError, IKibanaSearchRequest, IKibanaSearchResponse, ISearchOptions, ES_SEARCH_STRATEGY, ISessionService, + getCombinedSignal, } from '../../common'; import { SearchUsageCollector } from './collectors'; import { @@ -171,11 +171,12 @@ export class SearchInterceptor { ...(abortSignal ? [abortSignal] : []), ]; - const combinedSignal = getCombinedSignal(signals); + const { signal: combinedSignal, cleanup: cleanupCombinedSignal } = getCombinedSignal(signals); const cleanup = () => { subscription.unsubscribe(); + combinedSignal.removeEventListener('abort', cleanup); + cleanupCombinedSignal(); }; - combinedSignal.addEventListener('abort', cleanup); return { diff --git a/src/plugins/data/public/search/session_service.test.ts b/src/plugins/data/public/search/session_service.test.ts index dd64d187f47d6..bcfd06944d983 100644 --- a/src/plugins/data/public/search/session_service.test.ts +++ b/src/plugins/data/public/search/session_service.test.ts @@ -20,6 +20,7 @@ import { SessionService } from './session_service'; import { ISessionService } from '../../common'; import { coreMock } from '../../../../core/public/mocks'; +import { take, toArray } from 'rxjs/operators'; describe('Session service', () => { let sessionService: ISessionService; @@ -39,5 +40,20 @@ describe('Session service', () => { sessionService.clear(); expect(sessionService.getSessionId()).toBeUndefined(); }); + + it('Restores a session', async () => { + const sessionId = 'sessionId'; + sessionService.restore(sessionId); + expect(sessionService.getSessionId()).toBe(sessionId); + }); + + it('sessionId$ observable emits current value', async () => { + sessionService.restore('1'); + const emittedValues = sessionService.getSession$().pipe(take(3), toArray()).toPromise(); + sessionService.restore('2'); + sessionService.clear(); + + expect(await emittedValues).toEqual(['1', '2', undefined]); + }); }); }); diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts index 31524434af302..a172738812937 100644 --- a/src/plugins/data/public/search/session_service.ts +++ b/src/plugins/data/public/search/session_service.ts @@ -18,14 +18,16 @@ */ import uuid from 'uuid'; -import { Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subscription } from 'rxjs'; import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; -import { ISessionService } from '../../common/search'; import { ConfigSchema } from '../../config'; +import { ISessionService } from '../../common/search'; export class SessionService implements ISessionService { - private sessionId?: string; - private session$: Subject = new Subject(); + private session$ = new BehaviorSubject(undefined); + private get sessionId() { + return this.session$.getValue(); + } private appChangeSubscription$?: Subscription; private curApp?: string; @@ -68,13 +70,15 @@ export class SessionService implements ISessionService { } public start() { - this.sessionId = uuid.v4(); - this.session$.next(this.sessionId); - return this.sessionId; + this.session$.next(uuid.v4()); + return this.sessionId!; + } + + public restore(sessionId: string) { + this.session$.next(sessionId); } public clear() { - this.sessionId = undefined; - this.session$.next(this.sessionId); + this.session$.next(undefined); } } diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 0d544ac9ad16a..8c009576ff280 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -105,7 +105,6 @@ function FilterBarUI(props: Props) { isOpen={isAddFilterPopoverOpen} closePopover={() => setIsAddFilterPopoverOpen(false)} anchorPosition="downLeft" - withTitle panelPaddingSize="none" ownFocus={true} initialFocus=".filterEditor__hiddenItem" diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 018f41ab82bfc..48dbfea634256 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -358,7 +358,6 @@ export function FilterItem(props: Props) { }} button={badge} anchorPosition="downLeft" - withTitle={true} panelPaddingSize="none" > diff --git a/src/plugins/data/public/ui/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx index b97e0e33f2400..46dda2382a5ca 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_options.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_options.tsx @@ -166,7 +166,6 @@ class FilterOptionsUI extends Component { } anchorPosition="rightUp" panelPaddingSize="none" - withTitle repositionOnScroll > diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx index 4d51b173f6743..3957e59388acf 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -76,7 +76,6 @@ export function QueryLanguageSwitcher(props: Props) { button={button} isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(false)} - withTitle repositionOnScroll > diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx index ac88d2aa36696..d294ffca86341 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { @@ -86,7 +86,11 @@ export function ActionBar({ onChangeCount(newDocCount); } }; - + useEffect(() => { + if (newDocCount !== docCount && newDocCount === 0) { + setNewDocCount(docCount); + } + }, [docCount, newDocCount]); return (
@@ -145,7 +149,7 @@ export function ActionBar({ - + {isSuccessor ? ( - - + - - -
- - -
+ reason="contextApp.state.loadingStatus.anchor.reason" + default-step-size="contextApp.state.queryParameters.defaultStepSize" + predecessor-count="contextApp.state.queryParameters.predecessorCount" + predecessor-available="contextApp.state.rows.predecessors.length" + predecessor-status="contextApp.state.loadingStatus.predecessors.status" + on-change-predecessor-count="contextApp.actions.fetchGivenPredecessorRows" + successor-count="contextApp.state.queryParameters.successorCount" + successor-available="contextApp.state.rows.successors.length" + successor-status="contextApp.state.loadingStatus.successors.status" + on-change-successor-count="contextApp.actions.fetchGivenSuccessorRows" +> diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss new file mode 100644 index 0000000000000..87194d834827b --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss @@ -0,0 +1,5 @@ +.dscCxtAppContent { + border: none; + background-color: transparent; + box-shadow: none; +} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx index 25576a9072944..77dd0a6d647c6 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx @@ -24,6 +24,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; +import { ContextErrorMessage } from '../context_error_message'; describe('ContextAppLegacy test', () => { const hit = { @@ -53,6 +54,7 @@ describe('ContextAppLegacy test', () => { minimumVisibleRows: 5, indexPattern, status: 'loaded', + reason: 'no reason', defaultStepSize: 5, predecessorCount: 10, successorCount: 10, @@ -76,9 +78,19 @@ describe('ContextAppLegacy test', () => { const props = { ...defaultProps }; props.status = 'loading'; const component = mountWithIntl(); - expect(component.find('DocTableLegacy').length).toBe(0); + expect(component.find(DocTableLegacy).length).toBe(0); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(1); expect(component.find(ActionBar).length).toBe(2); }); + + it('renders error message', () => { + const props = { ...defaultProps }; + props.status = 'failed'; + props.reason = 'something went wrong'; + const component = mountWithIntl(); + expect(component.find(DocTableLegacy).length).toBe(0); + const errorMessage = component.find(ContextErrorMessage); + expect(errorMessage.length).toBe(1); + }); }); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index afb4a9a981e21..b5387ec51db81 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -16,9 +16,11 @@ * specific language governing permissions and limitations * under the License. */ +import './context_app_legacy.scss'; import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiPanel, EuiText } from '@elastic/eui'; +import { EuiPanel, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; +import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, DocTableLegacyProps, @@ -35,6 +37,7 @@ export interface ContextAppProps { minimumVisibleRows: number; sorting: string[]; status: string; + reason: string; defaultStepSize: number; predecessorCount: number; successorCount: number; @@ -56,6 +59,7 @@ function isLoading(status: string) { export function ContextAppLegacy(renderProps: ContextAppProps) { const status = renderProps.status; const isLoaded = status === LOADING_STATUS.LOADED; + const isFailed = status === LOADING_STATUS.FAILED; const actionBarProps = (type: string) => { const { @@ -111,18 +115,24 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return ( - - - {loadingFeedback()} - {isLoaded ? ( - -
- -
-
- ) : null} - -
+ {isFailed ? ( + + ) : ( + + + + {loadingFeedback()} + {isLoaded ? ( + +
+ +
+
+ ) : null} + +
+
+ )}
); } diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts index 4a315be513a0d..bc4b7c4babd21 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -27,6 +27,7 @@ export function createContextAppLegacy(reactDirective: any) { ['columns', { watchDepth: 'collection' }], ['minimumVisibleRows', { watchDepth: 'reference' }], ['status', { watchDepth: 'reference' }], + ['reason', { watchDepth: 'reference' }], ['defaultStepSize', { watchDepth: 'reference' }], ['predecessorCount', { watchDepth: 'reference' }], ['predecessorAvailable', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts b/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts deleted file mode 100644 index 925d560761a84..0000000000000 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { ContextErrorMessage } from './context_error_message'; - -export function createContextErrorMessageDirective(reactDirective: any) { - return reactDirective(ContextErrorMessage, [ - ['status', { watchDepth: 'reference' }], - ['reason', { watchDepth: 'reference' }], - ]); -} diff --git a/src/plugins/discover/public/application/components/context_error_message/index.ts b/src/plugins/discover/public/application/components/context_error_message/index.ts index f20f2ccf8afa0..dfa9602408f8f 100644 --- a/src/plugins/discover/public/application/components/context_error_message/index.ts +++ b/src/plugins/discover/public/application/components/context_error_message/index.ts @@ -18,4 +18,3 @@ */ export { ContextErrorMessage } from './context_error_message'; -export { createContextErrorMessageDirective } from './context_error_message_directive'; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 99dad418c04bd..a42e2412ae928 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -224,7 +224,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { legend={legend} options={toggleButtons(id)} idSelected={`${id}-${values[id]}`} - onChange={(optionId) => handleValueChange(id, optionId.replace(`${id}-`, ''))} + onChange={(optionId: string) => handleValueChange(id, optionId.replace(`${id}-`, ''))} buttonSize="compressed" isFullWidth data-test-subj={`${id}ButtonGroup`} diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 55a75240909bf..651a26cad755d 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -52,7 +52,6 @@ import { createTopNavDirective, createTopNavHelper, } from '../../kibana_legacy/public'; -import { createContextErrorMessageDirective } from './application/components/context_error_message'; import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive'; @@ -137,8 +136,7 @@ export function initializeInnerAngularModule( .config(watchMultiDecorator) .run(registerListenEventListener) .directive('renderComplete', createRenderCompleteDirective) - .directive('discoverLegacy', createDiscoverLegacyDirective) - .directive('contextErrorMessage', createContextErrorMessageDirective); + .directive('discoverLegacy', createDiscoverLegacyDirective); } function createLocalPromiseModule() { diff --git a/src/plugins/embeddable/public/components/panel_options_menu/index.tsx b/src/plugins/embeddable/public/components/panel_options_menu/index.tsx index 7790646a88a68..9b9d95628f19d 100644 --- a/src/plugins/embeddable/public/components/panel_options_menu/index.tsx +++ b/src/plugins/embeddable/public/components/panel_options_menu/index.tsx @@ -83,7 +83,6 @@ export const PanelOptionsMenu: React.FC = ({ panelPaddingSize="none" anchorPosition="downRight" data-test-subj={open ? 'embeddablePanelContextMenuOpen' : 'embeddablePanelContextMenuClosed'} - withTitle > diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 789353ca4abd7..0d2dcf208f2ef 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -70,6 +70,7 @@ export { isSavedObjectEmbeddableInput, isRangeSelectTriggerContext, isValueClickTriggerContext, + isContextMenuTriggerContext, EmbeddableStateTransfer, EmbeddableEditorState, EmbeddablePackageState, diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_options_menu.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_options_menu.tsx index 629a5f8c880e8..82bfb9b47e1a7 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_options_menu.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_options_menu.tsx @@ -113,7 +113,6 @@ export class PanelOptionsMenu extends React.Component = | ValueClickContext | RangeSelectContext; -export const isValueClickTriggerContext = ( - context: ChartActionContext -): context is ValueClickContext => context.data && 'data' in context.data; - -export const isRangeSelectTriggerContext = ( - context: ChartActionContext -): context is RangeSelectContext => context.data && 'range' in context.data; - export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { id: CONTEXT_MENU_TRIGGER, - title: 'Context menu', - description: 'Triggered on top-right corner context-menu select.', + title: i18n.translate('embeddableApi.contextMenuTrigger.title', { + defaultMessage: 'Context menu', + }), + description: i18n.translate('embeddableApi.contextMenuTrigger.description', { + defaultMessage: 'A panel top-right corner context menu click.', + }), }; export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = { id: PANEL_BADGE_TRIGGER, - title: 'Panel badges', - description: 'Actions appear in title bar when an embeddable loads in a panel.', + title: i18n.translate('embeddableApi.panelBadgeTrigger.title', { + defaultMessage: 'Panel badges', + }), + description: i18n.translate('embeddableApi.panelBadgeTrigger.description', { + defaultMessage: 'Actions appear in title bar when an embeddable loads in a panel.', + }), }; export const PANEL_NOTIFICATION_TRIGGER = 'PANEL_NOTIFICATION_TRIGGER'; export const panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'> = { id: PANEL_NOTIFICATION_TRIGGER, - title: 'Panel notifications', - description: 'Actions appear in top-right corner of a panel.', + title: i18n.translate('embeddableApi.panelNotificationTrigger.title', { + defaultMessage: 'Panel notifications', + }), + description: i18n.translate('embeddableApi.panelNotificationTrigger.description', { + defaultMessage: 'Actions appear in top-right corner of a panel.', + }), }; + +export const isValueClickTriggerContext = ( + context: ChartActionContext +): context is ValueClickContext => context.data && 'data' in context.data; + +export const isRangeSelectTriggerContext = ( + context: ChartActionContext +): context is RangeSelectContext => context.data && 'range' in context.data; + +export const isContextMenuTriggerContext = (context: unknown): context is EmbeddableContext => + !!context && + typeof context === 'object' && + !!(context as EmbeddableContext).embeddable && + typeof (context as EmbeddableContext).embeddable === 'object'; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 00971ed37db3a..e84dff1172c2e 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -695,6 +695,11 @@ export interface IEmbeddable): void; } +// Warning: (ae-missing-release-tag) "isContextMenuTriggerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext; + // Warning: (ae-missing-release-tag) "isErrorEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -884,7 +889,7 @@ export const withEmbeddableSubscription: (promise: Promise): Promise { - return Promise.race([this.abortRejection, promise]); + return Promise.race([this.abortRejection.promise, promise]); } /** @@ -189,14 +189,18 @@ export class Execution< else reject(error); }); - this.firstResultFuture.promise.then( - (result) => { - this.state.transitions.setResult(result); - }, - (error) => { - this.state.transitions.setError(error); - } - ); + this.firstResultFuture.promise + .then( + (result) => { + this.state.transitions.setResult(result); + }, + (error) => { + this.state.transitions.setError(error); + } + ) + .finally(() => { + this.abortRejection.cleanup(); + }); } async invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise { diff --git a/src/plugins/expressions/common/execution/execution_contract.test.ts b/src/plugins/expressions/common/execution/execution_contract.test.ts index 856b22470d782..eaf7e6ea862eb 100644 --- a/src/plugins/expressions/common/execution/execution_contract.test.ts +++ b/src/plugins/expressions/common/execution/execution_contract.test.ts @@ -59,7 +59,7 @@ describe('ExecutionContract', () => { test('can cancel execution', () => { const execution = createExecution('foo bar=123'); - const spy = jest.spyOn(execution, 'cancel'); + const spy = jest.spyOn(execution, 'cancel').mockImplementation(() => {}); const contract = new ExecutionContract(execution); expect(spy).toHaveBeenCalledTimes(0); diff --git a/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts b/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts index 970015638794f..672abadd3c016 100644 --- a/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts +++ b/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts @@ -19,7 +19,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; -import { Datatable, DatatableRow } from '../../expression_types'; +import { Datatable } from '../../expression_types'; +import { buildResultColumns, getBucketIdentifier } from './series_calculation_helpers'; export interface CumulativeSumArgs { by?: string[]; @@ -35,15 +36,6 @@ export type ExpressionFunctionCumulativeSum = ExpressionFunctionDefinition< Datatable >; -/** - * Returns a string identifying the group of a row by a list of columns to group by - */ -function getBucketIdentifier(row: DatatableRow, groupColumns?: string[]) { - return (groupColumns || []) - .map((groupColumnId) => (row[groupColumnId] == null ? '' : String(row[groupColumnId]))) - .join('|'); -} - /** * Calculates the cumulative sum of a specified column in the data table. * @@ -114,38 +106,17 @@ export const cumulativeSum: ExpressionFunctionCumulativeSum = { }, fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) { - if (input.columns.some((column) => column.id === outputColumnId)) { - throw new Error( - i18n.translate('expressions.functions.cumulativeSum.columnConflictMessage', { - defaultMessage: - 'Specified outputColumnId {columnId} already exists. Please pick another column id.', - values: { - columnId: outputColumnId, - }, - }) - ); - } - - const inputColumnDefinition = input.columns.find((column) => column.id === inputColumnId); + const resultColumns = buildResultColumns( + input, + outputColumnId, + inputColumnId, + outputColumnName + ); - if (!inputColumnDefinition) { + if (!resultColumns) { return input; } - const outputColumnDefinition = { - ...inputColumnDefinition, - id: outputColumnId, - name: outputColumnName || outputColumnId, - }; - - const resultColumns = [...input.columns]; - // add output column after input column in the table - resultColumns.splice( - resultColumns.indexOf(inputColumnDefinition) + 1, - 0, - outputColumnDefinition - ); - const accumulators: Partial> = {}; return { ...input, diff --git a/src/plugins/expressions/common/expression_functions/specs/derivative.ts b/src/plugins/expressions/common/expression_functions/specs/derivative.ts new file mode 100644 index 0000000000000..44ac198e2d17c --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/derivative.ts @@ -0,0 +1,148 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../types'; +import { Datatable } from '../../expression_types'; +import { buildResultColumns, getBucketIdentifier } from './series_calculation_helpers'; + +export interface DerivativeArgs { + by?: string[]; + inputColumnId: string; + outputColumnId: string; + outputColumnName?: string; +} + +export type ExpressionFunctionDerivative = ExpressionFunctionDefinition< + 'derivative', + Datatable, + DerivativeArgs, + Datatable +>; + +/** + * Calculates the derivative of a specified column in the data table. + * + * Also supports multiple series in a single data table - use the `by` argument + * to specify the columns to split the calculation by. + * For each unique combination of all `by` columns a separate derivative will be calculated. + * The order of rows won't be changed - this function is not modifying any existing columns, it's only + * adding the specified `outputColumnId` column to every row of the table without adding or removing rows. + * + * Behavior: + * * Will write the derivative of `inputColumnId` into `outputColumnId` + * * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId` + * * Derivative always start with an undefined value for the first row of a series, a cell will contain its own value minus the + * value of the previous cell of the same series. + * + * Edge cases: + * * Will return the input table if `inputColumnId` does not exist + * * Will throw an error if `outputColumnId` exists already in provided data table + * * If there is no previous row of the current series with a non `null` or `undefined` value, the output cell of the current row + * will be set to `undefined`. + * * If the row value contains `null` or `undefined`, it will be ignored and the output cell will be set to `undefined` + * * If the value of the previous row of the same series contains `null` or `undefined`, the output cell of the current row will be set to `undefined` as well + * * For all values besides `null` and `undefined`, the value will be cast to a number before it's used in the + * calculation of the current series even if this results in `NaN` (like in case of objects). + * * To determine separate series defined by the `by` columns, the values of these columns will be cast to strings + * before comparison. If the values are objects, the return value of their `toString` method will be used for comparison. + * Missing values (`null` and `undefined`) will be treated as empty strings. + */ +export const derivative: ExpressionFunctionDerivative = { + name: 'derivative', + type: 'datatable', + + inputTypes: ['datatable'], + + help: i18n.translate('expressions.functions.derivative.help', { + defaultMessage: 'Calculates the derivative of a column in a data table', + }), + + args: { + by: { + help: i18n.translate('expressions.functions.derivative.args.byHelpText', { + defaultMessage: 'Column to split the derivative calculation by', + }), + multi: true, + types: ['string'], + required: false, + }, + inputColumnId: { + help: i18n.translate('expressions.functions.derivative.args.inputColumnIdHelpText', { + defaultMessage: 'Column to calculate the derivative of', + }), + types: ['string'], + required: true, + }, + outputColumnId: { + help: i18n.translate('expressions.functions.derivative.args.outputColumnIdHelpText', { + defaultMessage: 'Column to store the resulting derivative in', + }), + types: ['string'], + required: true, + }, + outputColumnName: { + help: i18n.translate('expressions.functions.derivative.args.outputColumnNameHelpText', { + defaultMessage: 'Name of the column to store the resulting derivative in', + }), + types: ['string'], + required: false, + }, + }, + + fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) { + const resultColumns = buildResultColumns( + input, + outputColumnId, + inputColumnId, + outputColumnName + ); + + if (!resultColumns) { + return input; + } + + const previousValues: Partial> = {}; + return { + ...input, + columns: resultColumns, + rows: input.rows.map((row) => { + const newRow = { ...row }; + + const bucketIdentifier = getBucketIdentifier(row, by); + const previousValue = previousValues[bucketIdentifier]; + const currentValue = newRow[inputColumnId]; + + if (currentValue != null && previousValue != null) { + newRow[outputColumnId] = Number(currentValue) - previousValue; + } else { + newRow[outputColumnId] = undefined; + } + + if (currentValue != null) { + previousValues[bucketIdentifier] = Number(currentValue); + } else { + previousValues[bucketIdentifier] = undefined; + } + + return newRow; + }), + }; + }, +}; diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts index aadea5882b9c0..d414057598f12 100644 --- a/src/plugins/expressions/common/expression_functions/specs/index.ts +++ b/src/plugins/expressions/common/expression_functions/specs/index.ts @@ -26,6 +26,7 @@ import { variable } from './var'; import { AnyExpressionFunctionDefinition } from '../types'; import { theme } from './theme'; import { cumulativeSum } from './cumulative_sum'; +import { derivative } from './derivative'; export const functionSpecs: AnyExpressionFunctionDefinition[] = [ clog, @@ -36,6 +37,7 @@ export const functionSpecs: AnyExpressionFunctionDefinition[] = [ variable, theme, cumulativeSum, + derivative, ]; export * from './clog'; @@ -46,3 +48,4 @@ export * from './var_set'; export * from './var'; export * from './theme'; export * from './cumulative_sum'; +export * from './derivative'; diff --git a/src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts b/src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts new file mode 100644 index 0000000000000..8ba9d527d4c59 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Datatable, DatatableRow } from '../../expression_types'; + +/** + * Returns a string identifying the group of a row by a list of columns to group by + */ +export function getBucketIdentifier(row: DatatableRow, groupColumns?: string[]) { + return (groupColumns || []) + .map((groupColumnId) => (row[groupColumnId] == null ? '' : String(row[groupColumnId]))) + .join('|'); +} + +/** + * Checks whether input and output columns are defined properly + * and builds column array of the output table if that's the case. + * + * * Throws an error if the output column exists already. + * * Returns undefined if the input column doesn't exist. + * @param input Input datatable + * @param outputColumnId Id of the output column + * @param inputColumnId Id of the input column + * @param outputColumnName Optional name of the output column + */ +export function buildResultColumns( + input: Datatable, + outputColumnId: string, + inputColumnId: string, + outputColumnName: string | undefined +) { + if (input.columns.some((column) => column.id === outputColumnId)) { + throw new Error( + i18n.translate('expressions.functions.seriesCalculations.columnConflictMessage', { + defaultMessage: + 'Specified outputColumnId {columnId} already exists. Please pick another column id.', + values: { + columnId: outputColumnId, + }, + }) + ); + } + + const inputColumnDefinition = input.columns.find((column) => column.id === inputColumnId); + + if (!inputColumnDefinition) { + return; + } + + const outputColumnDefinition = { + ...inputColumnDefinition, + id: outputColumnId, + name: outputColumnName || outputColumnId, + }; + + const resultColumns = [...input.columns]; + // add output column after input column in the table + resultColumns.splice(resultColumns.indexOf(inputColumnDefinition) + 1, 0, outputColumnDefinition); + return resultColumns; +} diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/derivative.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/derivative.test.ts new file mode 100644 index 0000000000000..63b2df5382557 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/tests/derivative.test.ts @@ -0,0 +1,406 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { functionWrapper } from './utils'; +import { derivative, DerivativeArgs } from '../derivative'; +import { ExecutionContext } from '../../../execution/types'; +import { Datatable } from '../../../expression_types/specs/datatable'; + +describe('interpreter/functions#derivative', () => { + const fn = functionWrapper(derivative); + const runFn = (input: Datatable, args: DerivativeArgs) => + fn(input, args, {} as ExecutionContext) as Datatable; + + it('calculates derivative', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: 7 }, { val: 3 }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { type: 'number' }, + }); + expect(result.rows.map((row) => row.output)).toEqual([undefined, 2, -4, -1]); + }); + + it('skips null or undefined values until there is real data', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [ + {}, + { val: null }, + { val: undefined }, + { val: 1 }, + { val: 2 }, + { val: undefined }, + { val: undefined }, + { val: 4 }, + { val: 8 }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { type: 'number' }, + }); + expect(result.rows.map((row) => row.output)).toEqual([ + undefined, + undefined, + undefined, + undefined, + 2 - 1, + undefined, + undefined, + undefined, + 8 - 4, + ]); + }); + + it('treats 0 as real data', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [ + {}, + { val: null }, + { val: undefined }, + { val: 1 }, + { val: 2 }, + { val: 0 }, + { val: undefined }, + { val: 0 }, + { val: undefined }, + { val: 0 }, + { val: 8 }, + { val: 0 }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.rows.map((row) => row.output)).toEqual([ + undefined, + undefined, + undefined, + undefined, + 2 - 1, + 0 - 2, + undefined, + undefined, + undefined, + undefined, + 8 - 0, + 0 - 8, + ]); + }); + + it('calculates derivative for multiple series', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: 2, split: 'B' }, + { val: 3, split: 'B' }, + { val: 4, split: 'A' }, + { val: 5, split: 'A' }, + { val: 6, split: 'A' }, + { val: 7, split: 'B' }, + { val: 8, split: 'B' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'] } + ); + + expect(result.rows.map((row) => row.output)).toEqual([ + undefined, + undefined, + 3 - 2, + 4 - 1, + 5 - 4, + 6 - 5, + 7 - 3, + 8 - 7, + ]); + }); + + it('treats missing split column as separate series', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: 2, split: 'B' }, + { val: 3 }, + { val: 4, split: 'A' }, + { val: 5 }, + { val: 6, split: 'A' }, + { val: 7, split: 'B' }, + { val: 8, split: 'B' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'] } + ); + expect(result.rows.map((row) => row.output)).toEqual([ + undefined, + undefined, + undefined, + 4 - 1, + 5 - 3, + 6 - 4, + 7 - 2, + 8 - 7, + ]); + }); + + it('treats null like undefined and empty string for split columns', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A' }, + { val: 2, split: 'B' }, + { val: 3 }, + { val: 4, split: 'A' }, + { val: 5 }, + { val: 6, split: 'A' }, + { val: 7, split: null }, + { val: 8, split: 'B' }, + { val: 9, split: '' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'] } + ); + expect(result.rows.map((row) => row.output)).toEqual([ + undefined, + undefined, + undefined, + 4 - 1, + 5 - 3, + 6 - 4, + 7 - 5, + 8 - 2, + 9 - 7, + ]); + }); + + it('calculates derivative for multiple series by multiple split columns', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + { id: 'split2', name: 'split2', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: 'A', split2: 'C' }, + { val: 2, split: 'B', split2: 'C' }, + { val: 3, split2: 'C' }, + { val: 4, split: 'A', split2: 'C' }, + { val: 5 }, + { val: 6, split: 'A', split2: 'D' }, + { val: 7, split: 'B', split2: 'D' }, + { val: 8, split: 'B', split2: 'D' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split', 'split2'] } + ); + expect(result.rows.map((row) => row.output)).toEqual([ + undefined, + undefined, + undefined, + 4 - 1, + undefined, + undefined, + undefined, + 8 - 7, + ]); + }); + + it('splits separate series by the string representation of the cell values', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { id: 'val', name: 'val', meta: { type: 'number' } }, + { id: 'split', name: 'split', meta: { type: 'string' } }, + ], + rows: [ + { val: 1, split: { anObj: 3 } }, + { val: 2, split: { anotherObj: 5 } }, + { val: 10, split: 5 }, + { val: 11, split: '5' }, + ], + }, + { inputColumnId: 'val', outputColumnId: 'output', by: ['split'] } + ); + + expect(result.rows.map((row) => row.output)).toEqual([undefined, 2 - 1, undefined, 11 - 10]); + }); + + it('casts values to number before calculating derivative', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: '7' }, { val: '3' }, { val: 2 }], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, 3 - 7, 2 - 3]); + }); + + it('casts values to number before calculating derivative for NaN like values', () => { + const result = runFn( + { + type: 'datatable', + columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], + rows: [{ val: 5 }, { val: '7' }, { val: {} }, { val: 2 }, { val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, NaN, NaN, 5 - 2]); + }); + + it('copies over meta information from the source column', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + + field: 'afield', + index: 'anindex', + params: { id: 'number', params: { pattern: '000' } }, + source: 'synthetic', + sourceParams: { + some: 'params', + }, + }, + }, + ], + rows: [{ val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'output' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'output', + meta: { + type: 'number', + + field: 'afield', + index: 'anindex', + params: { id: 'number', params: { pattern: '000' } }, + source: 'synthetic', + sourceParams: { + some: 'params', + }, + }, + }); + }); + + it('sets output name on output column if specified', () => { + const result = runFn( + { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + }, + }, + ], + rows: [{ val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'output', outputColumnName: 'Output name' } + ); + expect(result.columns).toContainEqual({ + id: 'output', + name: 'Output name', + meta: { type: 'number' }, + }); + }); + + it('returns source table if input column does not exist', () => { + const input: Datatable = { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + }, + }, + ], + rows: [{ val: 5 }], + }; + expect(runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(input); + }); + + it('throws an error if output column exists already', () => { + expect(() => + runFn( + { + type: 'datatable', + columns: [ + { + id: 'val', + name: 'val', + meta: { + type: 'number', + }, + }, + ], + rows: [{ val: 5 }], + }, + { inputColumnId: 'val', outputColumnId: 'val' } + ) + ).toThrow(); + }); +}); diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index fb1823e85b391..134e9e3a63502 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -30,6 +30,7 @@ import { ExpressionFunctionVar, ExpressionFunctionTheme, ExpressionFunctionCumulativeSum, + ExpressionFunctionDerivative, } from './specs'; import { ExpressionAstFunction } from '../ast'; import { PersistableStateDefinition } from '../../../kibana_utils/common'; @@ -133,4 +134,5 @@ export interface ExpressionFunctionDefinitions { var: ExpressionFunctionVar; theme: ExpressionFunctionTheme; cumulative_sum: ExpressionFunctionCumulativeSum; + derivative: ExpressionFunctionDerivative; } diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 5ca085ebb0d1d..06b7bc2142447 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -389,6 +389,10 @@ export interface ExpressionFunctionDefinitions { // // (undocumented) cumulative_sum: ExpressionFunctionCumulativeSum; + // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDerivative" needs to be exported by the entry point index.d.ts + // + // (undocumented) + derivative: ExpressionFunctionDerivative; // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionFont" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index c8d5464929033..cacd61a8638a6 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -361,6 +361,10 @@ export interface ExpressionFunctionDefinitions { // // (undocumented) cumulative_sum: ExpressionFunctionCumulativeSum; + // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDerivative" needs to be exported by the entry point index.d.ts + // + // (undocumented) + derivative: ExpressionFunctionDerivative; // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionFont" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap index 9992fe2c8cf5f..2a4fb9e48a92b 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap @@ -175,29 +175,25 @@ exports[`bulkCreate should display error message when bulkCreate request fails 1
- - -
- - + + + Step 1 is incomplete + + + + - - -
- - - - - -
-
-
+ + + + diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.test.js b/src/plugins/home/public/application/components/tutorial/tutorial.test.js index 9944ac4848bc6..65e11170e0e42 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.test.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.test.js @@ -133,7 +133,7 @@ describe('isCloudEnabled is false', () => { ); await loadTutorialPromise; component.update(); - component.find('button#onPremElasticCloud').closest('div').find('input').simulate('change'); + component.find('#onPremElasticCloud').first().simulate('click'); component.update(); expect(component.state('visibleInstructions')).toBe('onPremElasticCloud'); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap index 6631a9bbd1d02..db527ea81b2cb 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap @@ -63,7 +63,6 @@ exports[`IndicesList should change pages 1`] = ` isOpen={false} ownFocus={false} panelPaddingSize="none" - withTitle={true} > diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index e0e295723a69d..b6efa40c5e40b 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -427,6 +427,7 @@ class TableListView extends React.Component !error, onClick: this.props.editItem, }, ]; diff --git a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx index 832ea70f0460e..5d26925d34088 100644 --- a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx +++ b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx @@ -105,15 +105,19 @@ export class ValidatedDualRange extends Component { allowEmptyRange, ...rest // TODO: Consider alternatives for spread operator in component } = this.props; + // Ensure the form row is display as compressed if compressed is true + let evaluatedDisplay = formRowDisplay; + if (!evaluatedDisplay) { + evaluatedDisplay = compressed ? 'rowCompressed' : 'row'; + } return ( { conflictedSavedObjectsLinkedToSavedSearches: undefined, conflictedSearchDocs: undefined, unmatchedReferences: undefined, + unmatchedReferencesTablePagination: { + pageIndex: 0, + pageSize: 5, + }, conflictingRecord: undefined, error: undefined, file: undefined, @@ -467,7 +472,7 @@ export class Flyout extends Component { }; renderUnmatchedReferences() { - const { unmatchedReferences } = this.state; + const { unmatchedReferences, unmatchedReferencesTablePagination: tablePagination } = this.state; if (!unmatchedReferences) { return null; @@ -527,22 +532,28 @@ export class Flyout extends Component { { defaultMessage: 'New index pattern' } ), render: (id: string) => { - const options = this.state.indexPatterns!.map( - (indexPattern) => - ({ - text: indexPattern.title, - value: indexPattern.id, - 'data-test-subj': `indexPatternOption-${indexPattern.title}`, - } as { text: string; value: string; 'data-test-subj'?: string }) - ); - - options.unshift({ - text: '-- Skip Import --', - value: '', - }); + const options = [ + { + text: '-- Skip Import --', + value: '', + }, + ...this.state.indexPatterns!.map( + (indexPattern) => + ({ + text: indexPattern.title, + value: indexPattern.id, + 'data-test-subj': `indexPatternOption-${indexPattern.title}`, + } as { text: string; value: string; 'data-test-subj'?: string }) + ), + ]; + + const selectedValue = + unmatchedReferences?.find((unmatchedRef) => unmatchedRef.existingIndexPatternId === id) + ?.newIndexPatternId ?? ''; return ( this.onIndexChanged(id, e)} options={options} @@ -553,6 +564,7 @@ export class Flyout extends Component { ]; const pagination = { + ...tablePagination, pageSizeOptions: [5, 10, 25], }; @@ -561,6 +573,16 @@ export class Flyout extends Component { items={unmatchedReferences as any[]} columns={columns} pagination={pagination} + onTableChange={({ page }) => { + if (page) { + this.setState({ + unmatchedReferencesTablePagination: { + pageSize: page.size, + pageIndex: page.index, + }, + }); + } + }} /> ); } diff --git a/src/plugins/share/public/services/share_menu_manager.tsx b/src/plugins/share/public/services/share_menu_manager.tsx index 3325c5503fe89..c1f8d55dfc76f 100644 --- a/src/plugins/share/public/services/share_menu_manager.tsx +++ b/src/plugins/share/public/services/share_menu_manager.tsx @@ -89,7 +89,6 @@ export class ShareMenuManager { isOpen={true} closePopover={this.onClose} panelPaddingSize="none" - withTitle anchorPosition="downLeft" > + <> {numbers.map((number, arrayIndex) => ( diff --git a/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx index 4afc85ba3b5c2..785ef1b83a23d 100644 --- a/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/date_ranges.tsx @@ -125,7 +125,7 @@ function DateRangesParamEditor({ ); return ( - + <> diff --git a/src/plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx index 41d6db25da5e2..9529adfe12720 100644 --- a/src/plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx @@ -108,7 +108,7 @@ function FieldParamEditor({ isInvalid={showErrorMessage} fullWidth={true} error={errors} - compressed + display="rowCompressed" > + {agg.params.ipRangeType === IpRangeTypes.MASK ? ( setValue(ev.target.value), [setValue]); return ( - + + + - + + <> {ranges.map(({ from, to, id }, index) => { const deleteBtnTitle = i18n.translate( diff --git a/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx b/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx index b433b704330a7..714bb3f3294ab 100644 --- a/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/raw_json.tsx @@ -82,7 +82,7 @@ function RawJsonParamEditor({ label={label} isInvalid={showValidation ? !isFieldValid : false} fullWidth={true} - compressed + display="rowCompressed" > <> - + + setAutoApplyEnabled(e.target.checked), []); + const toggleAutoApply = useCallback( + (nextAutoApplyEnabled) => setAutoApplyEnabled(nextAutoApplyEnabled), + [] + ); const onClickDiscard = useCallback(() => dispatch(discardChanges(vis)), [dispatch, vis]); useDebounce( @@ -131,19 +127,33 @@ function DefaultEditorControls({ defaultMessage: 'Auto updates the visualization on every change.', })} > - toggleAutoApply(!autoApplyEnabled)} size="s" - isIconOnly - /> + minWidth={80} + aria-label={ + autoApplyEnabled + ? i18n.translate('visDefaultEditor.sidebar.autoApplyChangesLabelOn', { + defaultMessage: 'Auto apply is on', + }) + : i18n.translate('visDefaultEditor.sidebar.autoApplyChangesLabelOff', { + defaultMessage: 'Auto apply is off', + }) + } + > + {autoApplyEnabled + ? i18n.translate('visDefaultEditor.sidebar.autoApplyChangesOn', { + defaultMessage: 'On', + }) + : i18n.translate('visDefaultEditor.sidebar.autoApplyChangesOff', { + defaultMessage: 'Off', + })} + )}
diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx index ac6ab9e9e69a8..d0a7412238871 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx @@ -86,7 +86,7 @@ function MetricVisOptions({ ); const setColorMode: EuiButtonGroupProps['onChange'] = useCallback( - (id) => setMetricValue('metricColorMode', id as ColorModes), + (id: string) => setMetricValue('metricColorMode', id as ColorModes), [setMetricValue] ); diff --git a/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx b/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx index 7ed98f0fb802e..e0a76316669f7 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx @@ -108,7 +108,7 @@ function TimelionInterval({ value, setValue, setValidity }: TimelionIntervalProp return ( + <> diff --git a/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx index 0823180300756..964bb7d569b08 100644 --- a/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx @@ -114,7 +114,7 @@ function ThresholdPanel({ defaultMessage: 'Line color', })} fullWidth - compressed + display="rowCompressed" > { + const [idToSelectedMap, setIdToSelectedMap] = useState({}); /** * Keydown listener for a legend entry. * This will close the details panel of this legend entry when pressing Escape. @@ -74,7 +75,7 @@ const VisLegendItemComponent = ({ } }; - const filterOptions: EuiButtonGroupOption[] = [ + const filterOptions: EuiButtonGroupOptionProps[] = [ { id: 'filterIn', label: i18n.translate('visTypeVislib.vislib.legend.filterForValueButtonAriaLabel', { @@ -96,6 +97,7 @@ const VisLegendItemComponent = ({ ]; const handleFilterChange = (id: string) => { + setIdToSelectedMap({ filterIn: id === 'filterIn', filterOut: id === 'filterOut' }); onFilter(item, id !== 'filterIn'); }; @@ -112,6 +114,7 @@ const VisLegendItemComponent = ({ options={filterOptions} onChange={handleFilterChange} data-test-subj={`legend-${item.label}-filters`} + idToSelectedMap={idToSelectedMap} /> diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index f6d27b54c7c64..9733e9fd68549 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -21,6 +21,7 @@ import { TriggerContextMapping } from '../../../ui_actions/public'; export interface VisualizationListItem { editUrl: string; editApp?: string; + error?: string; icon: string; id: string; stage: 'experimental' | 'beta' | 'production'; diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 02c0b36d12689..2089289b372a2 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -1032,6 +1032,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` size="s" > { .findListItems(filter, listingLimit) .then(({ total, hits }: { total: number; hits: object[] }) => ({ total, - hits: hits.filter((result: any) => isLabsEnabled || result.type.stage !== 'experimental'), + hits: hits.filter( + (result: any) => isLabsEnabled || result.type?.stage !== 'experimental' + ), })); }, [listingLimit, savedVisualizations, uiSettings] diff --git a/src/plugins/visualize/public/application/utils/get_table_columns.tsx b/src/plugins/visualize/public/application/utils/get_table_columns.tsx index 805c039d9773b..3541c0dc31db2 100644 --- a/src/plugins/visualize/public/application/utils/get_table_columns.tsx +++ b/src/plugins/visualize/public/application/utils/get_table_columns.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { History } from 'history'; -import { EuiBetaBadge, EuiButton, EuiEmptyPrompt, EuiIcon, EuiLink } from '@elastic/eui'; +import { EuiBetaBadge, EuiButton, EuiEmptyPrompt, EuiIcon, EuiLink, EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -87,20 +87,24 @@ export const getTableColumns = (application: ApplicationStart, history: History) defaultMessage: 'Title', }), sortable: true, - render: (field: string, { editApp, editUrl, title }: VisualizationListItem) => ( - { - if (editApp) { - application.navigateToApp(editApp, { path: editUrl }); - } else if (editUrl) { - history.push(editUrl); - } - }} - data-test-subj={`visListingTitleLink-${title.split(' ').join('-')}`} - > - {field} - - ), + render: (field: string, { editApp, editUrl, title, error }: VisualizationListItem) => + // In case an error occurs i.e. the vis has wrong type, we render the vis but without the link + !error ? ( + { + if (editApp) { + application.navigateToApp(editApp, { path: editUrl }); + } else if (editUrl) { + history.push(editUrl); + } + }} + data-test-subj={`visListingTitleLink-${title.split(' ').join('-')}`} + > + {field} + + ) : ( + field + ), }, { field: 'typeTitle', @@ -108,13 +112,18 @@ export const getTableColumns = (application: ApplicationStart, history: History) defaultMessage: 'Type', }), sortable: true, - render: (field: string, record: VisualizationListItem) => ( - - {renderItemTypeIcon(record)} - {record.typeTitle} - {getBadge(record)} - - ), + render: (field: string, record: VisualizationListItem) => + !record.error ? ( + + {renderItemTypeIcon(record)} + {record.typeTitle} + {getBadge(record)} + + ) : ( + + {record.error} + + ), }, { field: 'description', diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index e727949da5ad3..38ed3edc7deb5 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -35,7 +35,8 @@ export default function ({ getService, getPageObjects }) { // https://www.elastic.co/guide/en/kibana/current/tutorial-load-dataset.html - describe('Shakespeare', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/82206 + describe.skip('Shakespeare', function describeIndexTests() { // index starts on the first "count" metric at 1 // Each new metric or aggregation added to a visualization gets the next index. // So to modify a metric or aggregation tests need to keep track of the diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 0b417d7d23e93..ddabb32bf5909 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -421,21 +421,40 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(isSavedObjectImported).to.be(true); }); - it('should import saved objects with index patterns when index patterns does not exists', async () => { - // First, we need to delete the index pattern - await PageObjects.savedObjects.clickCheckboxByTitle('logstash-*'); - await PageObjects.savedObjects.clickDelete(); - - // Then, import the objects + it('should preserve index patterns selection when switching between pages', async () => { await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json') + path.join(__dirname, 'exports', '_import_objects_missing_all_index_patterns.json') ); - await PageObjects.savedObjects.checkImportSucceeded(); - await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.savedObjects.getRowTitles(); - const isSavedObjectImported = objects.includes('saved object imported with index pattern'); - expect(isSavedObjectImported).to.be(true); + await PageObjects.savedObjects.setOverriddenIndexPatternValue( + 'missing-index-pattern-1', + 'index-pattern-test-1' + ); + + await testSubjects.click('pagination-button-next'); + + await PageObjects.savedObjects.setOverriddenIndexPatternValue( + 'missing-index-pattern-7', + 'index-pattern-test-2' + ); + + await testSubjects.click('pagination-button-previous'); + + const selectedIdForMissingIndexPattern1 = await testSubjects.getAttribute( + 'managementChangeIndexSelection-missing-index-pattern-1', + 'value' + ); + + expect(selectedIdForMissingIndexPattern1).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a20'); + + await testSubjects.click('pagination-button-next'); + + const selectedIdForMissingIndexPattern7 = await testSubjects.getAttribute( + 'managementChangeIndexSelection-missing-index-pattern-7', + 'value' + ); + + expect(selectedIdForMissingIndexPattern7).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a87'); }); }); }); diff --git a/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json b/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json new file mode 100644 index 0000000000000..45572b0bf34fe --- /dev/null +++ b/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json @@ -0,0 +1,121 @@ +[ + { + "_id": "test-vis-1", + "_type": "visualization", + "_source": { + "title": "Test VIS 1", + "visState": "{\"title\":\"test vis 1\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-1\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-2", + "_type": "visualization", + "_source": { + "title": "Test VIS 2", + "visState": "{\"title\":\"test vis 2\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-2\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-3", + "_type": "visualization", + "_source": { + "title": "Test VIS 3", + "visState": "{\"title\":\"test vis 3\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-3\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-4", + "_type": "visualization", + "_source": { + "title": "Test VIS 4", + "visState": "{\"title\":\"test vis 4\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-4\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-5", + "_type": "visualization", + "_source": { + "title": "Test VIS 5", + "visState": "{\"title\":\"test vis 5\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-5\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-6", + "_type": "visualization", + "_source": { + "title": "Test VIS 6", + "visState": "{\"title\":\"test vis 6\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-6\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-7", + "_type": "visualization", + "_source": { + "title": "Test VIS 7", + "visState": "{\"title\":\"test vis 7\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-7\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + } +] diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index 8e65b6488836c..0742f14b2eeb4 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -126,6 +126,12 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv } } + async setOverriddenIndexPatternValue(oldName: string, newName: string) { + const select = await testSubjects.find(`managementChangeIndexSelection-${oldName}`); + const option = await testSubjects.findDescendant(`indexPatternOption-${newName}`, select); + await option.click(); + } + async clickCopyToSpaceByTitle(title: string) { const table = keyBy(await this.getElementsInTable(), 'title'); // should we check if table size > 0 and log error if not? diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index 8cac43d97317b..cdbbeb9188732 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -97,9 +97,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } public async clickSplitDirection(direction: string) { - const radioBtn = await find.byCssSelector( - `[data-test-subj="visEditorSplitBy"][title="${direction}"]` - ); + const radioBtn = await find.byCssSelector(`[data-test-subj="visEditorSplitBy-${direction}"]`); await radioBtn.click(); } @@ -334,10 +332,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } public async toggleAutoMode() { - // this is a temporary solution, should be replaced with initial after fixing the EuiToggleButton - // passing the data-test-subj attribute to a checkbox - await find.clickByCssSelector('.visEditorSidebar__controls input[type="checkbox"]'); - // await testSubjects.click('visualizeEditorAutoButton'); + await testSubjects.click('visualizeEditorAutoButton'); } public async isApplyEnabled() { diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 7236ac8afbda6..1edec496f1054 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "29.5.0", + "@elastic/eui": "30.1.1", "@kbn/plugin-helpers": "1.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json index 98a8f050bddd3..58cb4de0d63a4 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "29.5.0", + "@elastic/eui": "30.1.1", "react": "^16.12.0", "typescript": "4.0.2" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 8c0815b896a0e..7513e763bad7e 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "29.5.0", + "@elastic/eui": "30.1.1", "@kbn/plugin-helpers": "1.0.0", "react": "^16.12.0", "typescript": "4.0.2" diff --git a/tsconfig.json b/tsconfig.json index 30b38d0fc2dd3..2e4d5730d9320 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", "src/plugins/newsfeed/**/*", + "src/plugins/share/**/*", "src/plugins/telemetry_collection_manager/**/*", "src/plugins/telemetry/**/*", "src/plugins/url_forwarding/**/*", @@ -31,6 +32,7 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index c16e7a5e1b0f1..a37aa7b5b57fb 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -8,6 +8,7 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/containers/drilldowns_with_embeddable_example/drilldowns_with_embeddable_example.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/containers/drilldowns_with_embeddable_example/drilldowns_with_embeddable_example.tsx index a90147d01e8b6..48b64a1c84588 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/containers/drilldowns_with_embeddable_example/drilldowns_with_embeddable_example.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/containers/drilldowns_with_embeddable_example/drilldowns_with_embeddable_example.tsx @@ -89,7 +89,6 @@ export const DrilldownsWithEmbeddableExample: React.FC = () => { isOpen={openPopup} closePopover={() => setOpenPopup(false)} panelPaddingSize="none" - withTitle anchorPosition="downLeft" > diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/containers/drilldowns_without_embeddable_example/drilldowns_without_embeddable_example.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/containers/drilldowns_without_embeddable_example/drilldowns_without_embeddable_example.tsx index fb22e98e4a6d9..8f1582aaacff5 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/containers/drilldowns_without_embeddable_example/drilldowns_without_embeddable_example.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/containers/drilldowns_without_embeddable_example/drilldowns_without_embeddable_example.tsx @@ -77,7 +77,6 @@ export const DrilldownsWithoutEmbeddableExample: React.FC = () => { isOpen={openPopup} closePopover={() => setOpenPopup(false)} panelPaddingSize="none" - withTitle anchorPosition="downLeft" > diff --git a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json index f8c1a6b53dac5..05e5f39d4d628 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json +++ b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json @@ -16,5 +16,6 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" } ] } diff --git a/x-pack/package.json b/x-pack/package.json index ba464a21263d7..ea1b7b9713df9 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -267,7 +267,7 @@ "@babel/runtime": "^7.11.2", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.10.0", - "@elastic/eui": "29.5.0", + "@elastic/eui": "30.1.1", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index d0c7bf350504b..599e7461ea312 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -86,9 +86,10 @@ export interface PluginSetupContract { registerType< Config extends ActionTypeConfig = ActionTypeConfig, Secrets extends ActionTypeSecrets = ActionTypeSecrets, - Params extends ActionTypeParams = ActionTypeParams + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void >( - actionType: ActionType + actionType: ActionType ): void; } @@ -254,9 +255,10 @@ export class ActionsPlugin implements Plugin, Plugi registerType: < Config extends ActionTypeConfig = ActionTypeConfig, Secrets extends ActionTypeSecrets = ActionTypeSecrets, - Params extends ActionTypeParams = ActionTypeParams + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void >( - actionType: ActionType + actionType: ActionType ) => { if (!(actionType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${actionType.minimumLicenseRequired}" is not a valid license type`); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts index ab2bf20b36ed4..50c620dca9ddf 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts @@ -11,7 +11,7 @@ import { loginAndWaitForPage } from '../../integration/helpers'; export const DEFAULT_TIMEOUT = 60 * 1000; Given(`a user browses the APM UI application`, () => { - // open service overview page + // Open service inventory page loginAndWaitForPage(`/app/apm/services`, { from: '2020-06-01T14:59:32.686Z', to: '2020-06-16T16:59:36.219Z', diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts index d8540c3f3efd7..452d8b719b3cb 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts @@ -12,7 +12,7 @@ import { verifyClientMetrics } from './client_metrics_helper'; export const DEFAULT_TIMEOUT = { timeout: 60 * 1000 }; Given(`a user browses the APM UI application for RUM Data`, () => { - // open service overview page + // Open UX landing page const RANGE_FROM = 'now-24h'; const RANGE_TO = 'now'; loginAndWaitForPage( diff --git a/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/PopoverExpression/index.tsx b/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/PopoverExpression/index.tsx index ac96951ab54ca..a95ea3cf11e7a 100644 --- a/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/PopoverExpression/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/PopoverExpression/index.tsx @@ -20,7 +20,6 @@ export function PopoverExpression(props: Props) { return ( setPopoverOpen(false)} button={ diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index 446f7b978a434..c4224bf676abb 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -20,12 +20,12 @@ import { ApmHeader } from '../../shared/ApmHeader'; import { EuiTabLink } from '../../shared/EuiTabLink'; import { AnomalyDetectionSetupLink } from '../../shared/Links/apm/AnomalyDetectionSetupLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; -import { ServiceOverviewLink } from '../../shared/Links/apm/ServiceOverviewLink'; +import { ServiceInventoryLink } from '../../shared/Links/apm/service_inventory_link'; import { SettingsLink } from '../../shared/Links/apm/SettingsLink'; import { TraceOverviewLink } from '../../shared/Links/apm/TraceOverviewLink'; import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; import { ServiceMap } from '../ServiceMap'; -import { ServiceOverview } from '../ServiceOverview'; +import { ServiceInventory } from '../service_inventory'; import { TraceOverview } from '../TraceOverview'; import { AlertingPopoverAndFlyout } from './alerting_popover_flyout'; @@ -37,13 +37,13 @@ function getHomeTabs({ const homeTabs = [ { link: ( - + {i18n.translate('xpack.apm.home.servicesTabLabel', { defaultMessage: 'Services', })} - + ), - render: () => , + render: () => , name: 'services', }, { diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx deleted file mode 100644 index 21681f42d4b52..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; -import { NoServicesMessage } from '../NoServicesMessage'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; - -describe('NoServicesMessage', () => { - Object.values(FETCH_STATUS).forEach((status) => { - [true, false].forEach((historicalDataFound) => { - it(`status: ${status} and historicalDataFound: ${historicalDataFound}`, () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap deleted file mode 100644 index d027422961c99..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap +++ /dev/null @@ -1,99 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NoServicesMessage status: failure and historicalDataFound: false 1`] = ``; - -exports[`NoServicesMessage status: failure and historicalDataFound: true 1`] = ``; - -exports[`NoServicesMessage status: loading and historicalDataFound: false 1`] = ``; - -exports[`NoServicesMessage status: loading and historicalDataFound: true 1`] = ``; - -exports[`NoServicesMessage status: pending and historicalDataFound: false 1`] = ` - - } - body={ - -

- Upgrading from a pre-7.x version? Make sure you've also upgraded - your APM Server instance(s) to at least 7.0. -

-

- You may also have old data that needs to be migrated. - - - Learn more by visiting the Kibana Upgrade Assistant - - . -

-
- } - title={ -
- Looks like you don't have any APM services installed. Let's add some! -
- } - titleSize="s" -/> -`; - -exports[`NoServicesMessage status: pending and historicalDataFound: true 1`] = ` - - No services found -
- } - titleSize="s" -/> -`; - -exports[`NoServicesMessage status: success and historicalDataFound: false 1`] = ` - - } - body={ - -

- Upgrading from a pre-7.x version? Make sure you've also upgraded - your APM Server instance(s) to at least 7.0. -

-

- You may also have old data that needs to be migrated. - - - Learn more by visiting the Kibana Upgrade Assistant - - . -

-
- } - title={ -
- Looks like you don't have any APM services installed. Let's add some! -
- } - titleSize="s" -/> -`; - -exports[`NoServicesMessage status: success and historicalDataFound: true 1`] = ` - - No services found -
- } - titleSize="s" -/> -`; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/service_overview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/service_overview.test.tsx.snap deleted file mode 100644 index 49f30910ee0f1..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/service_overview.test.tsx.snap +++ /dev/null @@ -1,690 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Service Overview -> View should render empty message, when list is empty and historical data is found 1`] = ` -NodeList [ - - -
- -
- -
- No services found -
-
- -
-
-
- - , -] -`; - -exports[`Service Overview -> View should render getting started message, when list is empty and no historical data is found 1`] = ` -NodeList [ - - -
- -
- -
- Looks like you don't have any APM services installed. Let's add some! -
-
-
-

- Upgrading from a pre-7.x version? Make sure you've also upgraded - your APM Server instance(s) to at least 7.0. -

-

- You may also have old data that needs to be migrated. - - - Learn more by visiting the Kibana Upgrade Assistant - - . -

-
- - - - , -] -`; - -exports[`Service Overview -> View should render services, when list is not empty 1`] = ` -NodeList [ - .c1 { - font-size: 16px; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.c0 { - width: 100%; -} - -.c0 .apmServiceList__serviceNameTooltip { - width: 100%; -} - -.c0 .apmServiceList__serviceNameTooltip .apmServiceList__serviceNameContainer { - width: calc(100% - 24px); -} - - - -
- Health -
-
- - - - Warning - - - -
- - -
- Name -
-
- - - - - -
- - -
- Environment -
-
- - - - - 2 environments - - - - -
- - -
- Avg. response time -
-
-
-
-
-
- -
-
-
-
- N/A -
-
-
-
-
-
- 0 ms -
-
-
- - -
- Trans. per minute -
-
-
-
-
-
- -
-
-
-
- N/A -
-
-
-
-
-
- 0 tpm -
-
-
- - -
- Error rate % -
-
-
-
-
-
- -
-
-
-
- N/A -
-
-
-
-
-
-
-
- - , - .c1 { - font-size: 16px; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.c0 { - width: 100%; -} - -.c0 .apmServiceList__serviceNameTooltip { - width: 100%; -} - -.c0 .apmServiceList__serviceNameTooltip .apmServiceList__serviceNameContainer { - width: calc(100% - 24px); -} - - - -
- Health -
-
- - - - Unknown - - - -
- - -
- Name -
-
- - -
-
- go -
- -
-
-
-
- - -
- Environment -
-
- - -
- Avg. response time -
-
-
-
-
-
- -
-
-
-
- N/A -
-
-
-
-
-
- 0 ms -
-
-
- - -
- Trans. per minute -
-
-
-
-
-
- -
-
-
-
- N/A -
-
-
-
-
-
- 0 tpm -
-
-
- - -
- Error rate % -
-
-
-
-
-
- -
-
-
-
- N/A -
-
-
-
-
-
-
-
- - , -] -`; diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index aa71050802215..e770116ac2759 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -36,8 +36,8 @@ export function Settings({ children, location }: SettingsProps) { <> - {i18n.translate('xpack.apm.settings.returnToOverviewLinkLabel', { - defaultMessage: 'Return to overview', + {i18n.translate('xpack.apm.settings.returnLinkLabel', { + defaultMessage: 'Return to inventory', })} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx rename to x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/MLCallout.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx rename to x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/MLCallout.tsx index dd632db0f15fe..67eb22d48aae6 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/MLCallout.tsx @@ -15,7 +15,7 @@ import { APMLink } from '../../../shared/Links/apm/APMLink'; export function MLCallout({ onDismiss }: { onDismiss: () => void }) { return ( - {i18n.translate('xpack.apm.serviceOverview.toastText', { + {i18n.translate('xpack.apm.serviceInventory.toastText', { defaultMessage: "You're running Elastic Stack 7.0+ and we've detected incompatible data from a previous 6.x version. If you want to view this data in APM, you should migrate it. See more in ", })} @@ -74,7 +74,7 @@ export function ServiceOverview() { })} > {i18n.translate( - 'xpack.apm.serviceOverview.upgradeAssistantLink', + 'xpack.apm.serviceInventory.upgradeAssistantLinkText', { defaultMessage: 'the upgrade assistant', } @@ -86,6 +86,10 @@ export function ServiceOverview() { } }, [data.hasLegacyData, core.http.basePath, core.notifications.toasts]); + // The page is called "service inventory" to avoid confusion with the + // "service overview", but this is tracked in some dashboards because it's the + // initial landing page for APM, so it stays as "services_overview" (plural.) + // for backward compatibility. useTrackPageview({ app: 'apm', path: 'services_overview' }); useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 }); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx new file mode 100644 index 0000000000000..0fc2a2b4cdcef --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { NoServicesMessage } from './no_services_message'; + +function Wrapper({ children }: { children?: ReactNode }) { + return {children}; +} + +describe('NoServicesMessage', () => { + Object.values(FETCH_STATUS).forEach((status) => { + [true, false].forEach((historicalDataFound) => { + describe(`when status is ${status}`, () => { + describe(`when historicalDataFound is ${historicalDataFound}`, () => { + it('renders', () => { + expect(() => + render( + , + { wrapper: Wrapper } + ) + ).not.toThrowError(); + }); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx rename to x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx similarity index 79% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/service_overview.test.tsx rename to x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index 06e9008d5aebe..d394c7db62554 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -7,22 +7,22 @@ import { render, waitFor } from '@testing-library/react'; import { CoreStart } from 'kibana/public'; import { merge } from 'lodash'; -import React, { FunctionComponent, ReactChild } from 'react'; +import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import { ServiceHealthStatus } from '../../../../../common/service_health_status'; -import { ServiceOverview } from '..'; -import { EuiThemeProvider } from '../../../../../../observability/public'; -import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; +import { ServiceHealthStatus } from '../../../../common/service_health_status'; +import { ServiceInventory } from '.'; +import { EuiThemeProvider } from '../../../../../observability/public'; +import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import * as useAnomalyDetectionJobs from '../../../../hooks/useAnomalyDetectionJobs'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; -import * as urlParamsHooks from '../../../../hooks/useUrlParams'; -import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock'; +} from '../../../context/ApmPluginContext/MockApmPluginContext'; +import * as useAnomalyDetectionJobs from '../../../hooks/useAnomalyDetectionJobs'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import * as useLocalUIFilters from '../../../hooks/useLocalUIFilters'; +import * as urlParamsHooks from '../../../hooks/useUrlParams'; +import { SessionStorageMock } from '../../../services/__test__/SessionStorageMock'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, @@ -31,7 +31,7 @@ const KibanaReactContext = createKibanaReactContext({ const addWarning = jest.fn(); const httpGet = jest.fn(); -function wrapper({ children }: { children: ReactChild }) { +function wrapper({ children }: { children?: ReactNode }) { const mockPluginContext = (merge({}, mockApmPluginContextValue, { core: { http: { @@ -58,13 +58,7 @@ function wrapper({ children }: { children: ReactChild }) { ); } -function renderServiceOverview() { - return render(, { wrapper } as { - wrapper: FunctionComponent<{}>; - }); -} - -describe('Service Overview -> View', () => { +describe('ServiceInventory', () => { beforeEach(() => { // @ts-expect-error global.sessionStorage = new SessionStorageMock(); @@ -129,13 +123,13 @@ describe('Service Overview -> View', () => { ], }); - const { container, findByText } = renderServiceOverview(); + const { container, findByText } = render(, { wrapper }); // wait for requests to be made await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); await findByText('My Python Service'); - expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot(); + expect(container.querySelectorAll('.euiTableRow')).toHaveLength(2); }); it('should render getting started message, when list is empty and no historical data is found', async () => { @@ -145,17 +139,17 @@ describe('Service Overview -> View', () => { items: [], }); - const { container, findByText } = renderServiceOverview(); + const { findByText } = render(, { wrapper }); // wait for requests to be made await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); // wait for elements to be rendered - await findByText( + const gettingStartedMessage = await findByText( "Looks like you don't have any APM services installed. Let's add some!" ); - expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot(); + expect(gettingStartedMessage).not.toBeEmpty(); }); it('should render empty message, when list is empty and historical data is found', async () => { @@ -165,13 +159,13 @@ describe('Service Overview -> View', () => { items: [], }); - const { container, findByText } = renderServiceOverview(); + const { findByText } = render(, { wrapper }); // wait for requests to be made await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); - await findByText('No services found'); + const noServicesText = await findByText('No services found'); - expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot(); + expect(noServicesText).not.toBeEmpty(); }); describe('when legacy data is found', () => { @@ -182,7 +176,7 @@ describe('Service Overview -> View', () => { items: [], }); - renderServiceOverview(); + render(, { wrapper }); // wait for requests to be made await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); @@ -203,7 +197,7 @@ describe('Service Overview -> View', () => { items: [], }); - renderServiceOverview(); + render(, { wrapper }); // wait for requests to be made await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); @@ -229,7 +223,7 @@ describe('Service Overview -> View', () => { ], }); - const { queryByText } = renderServiceOverview(); + const { queryByText } = render(, { wrapper }); // wait for requests to be made await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); @@ -256,7 +250,7 @@ describe('Service Overview -> View', () => { ], }); - const { queryAllByText } = renderServiceOverview(); + const { queryAllByText } = render(, { wrapper }); // wait for requests to be made await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx similarity index 90% rename from x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx index 2081fc4767903..e3fa03a4d4f86 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx @@ -14,7 +14,7 @@ import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -function ServiceOverviewLink(props: APMLinkExtendProps) { +function ServiceInventoryLink(props: APMLinkExtendProps) { const { urlParams } = useUrlParams(); const persistedFilters = pickKeys(urlParams, 'host', 'agentName'); @@ -22,4 +22,4 @@ function ServiceOverviewLink(props: APMLinkExtendProps) { return ; } -export { ServiceOverviewLink }; +export { ServiceInventoryLink }; diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 560a1a077931b..cc0151afba63c 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -81,14 +81,14 @@ export class ApmPlugin implements Plugin { if (plugins.observability) { const getApmDataHelper = async () => { const { - fetchOverviewPageData, + fetchObservabilityOverviewPageData, hasData, createCallApmApi, - } = await import('./services/rest/apm_overview_fetchers'); + } = await import('./services/rest/apm_observability_overview_fetchers'); // have to do this here as well in case app isn't mounted yet createCallApmApi(core.http); - return { fetchOverviewPageData, hasData }; + return { fetchObservabilityOverviewPageData, hasData }; }; plugins.observability.dashboard.register({ appName: 'apm', @@ -98,7 +98,7 @@ export class ApmPlugin implements Plugin { }, fetchData: async (params: FetchDataParams) => { const dataHelper = await getApmDataHelper(); - return await dataHelper.fetchOverviewPageData(params); + return await dataHelper.fetchObservabilityOverviewPageData(params); }, }); diff --git a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts similarity index 89% rename from x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts rename to x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts index 4e306c93805d0..22ec317fca64b 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts @@ -5,7 +5,10 @@ */ import moment from 'moment'; -import { fetchOverviewPageData, hasData } from './apm_overview_fetchers'; +import { + fetchObservabilityOverviewPageData, + hasData, +} from './apm_observability_overview_fetchers'; import * as createCallApmApi from './createCallApmApi'; describe('Observability dashboard data', () => { @@ -37,7 +40,7 @@ describe('Observability dashboard data', () => { }); }); - describe('fetchOverviewPageData', () => { + describe('fetchObservabilityOverviewPageData', () => { it('returns APM data with series and stats', async () => { callApmApiMock.mockImplementation(() => Promise.resolve({ @@ -49,7 +52,7 @@ describe('Observability dashboard data', () => { ], }) ); - const response = await fetchOverviewPageData(params); + const response = await fetchObservabilityOverviewPageData(params); expect(response).toEqual({ appLink: '/app/apm/services?rangeFrom=now-15m&rangeTo=now', stats: { @@ -80,7 +83,7 @@ describe('Observability dashboard data', () => { transactionCoordinates: [], }) ); - const response = await fetchOverviewPageData(params); + const response = await fetchObservabilityOverviewPageData(params); expect(response).toEqual({ appLink: '/app/apm/services?rangeFrom=now-15m&rangeTo=now', stats: { @@ -107,7 +110,7 @@ describe('Observability dashboard data', () => { transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], }) ); - const response = await fetchOverviewPageData(params); + const response = await fetchObservabilityOverviewPageData(params); expect(response).toEqual({ appLink: '/app/apm/services?rangeFrom=now-15m&rangeTo=now', stats: { diff --git a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts similarity index 96% rename from x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts rename to x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index 422c7b882e5dc..bc1db4eed1d9e 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -13,7 +13,7 @@ import { callApmApi } from './createCallApmApi'; export { createCallApmApi } from './createCallApmApi'; -export const fetchOverviewPageData = async ({ +export const fetchObservabilityOverviewPageData = async ({ absoluteTime, relativeTime, bucketSize, diff --git a/x-pack/plugins/apm/public/setHelpExtension.ts b/x-pack/plugins/apm/public/setHelpExtension.ts index b6e745dcf1655..f895fbc36ed03 100644 --- a/x-pack/plugins/apm/public/setHelpExtension.ts +++ b/x-pack/plugins/apm/public/setHelpExtension.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import url from 'url'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; @@ -20,10 +19,7 @@ export function setHelpExtension({ chrome, http }: CoreStart) { }, { linkType: 'custom', - href: url.format({ - pathname: http.basePath.prepend('/app/kibana'), - hash: '/management/stack/upgrade_assistant', - }), + href: http.basePath.prepend('/app/management/stack/upgrade_assistant'), content: i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { defaultMessage: 'Upgrade assistant', }), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js index d7c7cd9e1a32f..9bdb66cc16c06 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js @@ -131,6 +131,7 @@ class ImageUpload extends React.Component { onChange={this.changeUrlType} isFullWidth className="canvasSidebar__buttonGroup" + legend={strings.getUrlTypeChangeLegend()} /> ); diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 51c86f6604330..b7d25a32818f8 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -870,6 +870,10 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.textStylePicker.alignRightOption', { defaultMessage: 'Align right', }), + getAlignmentOptionsControlLegend: () => + i18n.translate('xpack.canvas.textStylePicker.alignmentOptionsControl', { + defaultMessage: 'Alignment options', + }), getFontColorLabel: () => i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', { defaultMessage: 'Font Color', @@ -886,6 +890,10 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.textStylePicker.styleUnderlineOption', { defaultMessage: 'Underline', }), + getStyleOptionsControlLegend: () => + i18n.translate('xpack.canvas.textStylePicker.styleOptionsControl', { + defaultMessage: 'Style options', + }), }, TimePicker: { getApplyButtonLabel: () => @@ -1061,6 +1069,10 @@ export const ComponentStrings = { }), }, VarConfigVarValueField: { + getBooleanOptionsLegend: () => + i18n.translate('xpack.canvas.varConfigVarValueField.booleanOptionsLegend', { + defaultMessage: 'Boolean value', + }), getFalseOption: () => i18n.translate('xpack.canvas.varConfigVarValueField.falseOption', { defaultMessage: 'False', diff --git a/x-pack/plugins/canvas/i18n/ui.ts b/x-pack/plugins/canvas/i18n/ui.ts index bc282db203be2..0347a9c5444aa 100644 --- a/x-pack/plugins/canvas/i18n/ui.ts +++ b/x-pack/plugins/canvas/i18n/ui.ts @@ -181,6 +181,10 @@ export const ArgumentStrings = { url: URL, }, }), + getUrlTypeChangeLegend: () => + i18n.translate('xpack.canvas.uis.arguments.imageUpload.urlTypes.changeLegend', { + defaultMessage: 'Image upload type', + }), }, Number: { getDisplayName: () => diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot index 8225f6414385d..a6dbe675ee9d6 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot @@ -146,6 +146,7 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` aria-labelledby="CanvasAssetManagerLabel" className="euiProgress euiProgress--native euiProgress--s euiProgress--secondary" max={25000} + style={null} value={0} />
@@ -163,6 +164,11 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = `
@@ -654,6 +661,11 @@ exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` -
-
+ + -
-
+ + -
+ +
@@ -366,110 +354,122 @@ exports[`Storyshots components/TextStylePicker default 1`] = ` className="euiFlexItem euiFlexItem--flexGrowZero" >
+ + Alignment options +
-
- - -
-
+ +
-
+ +
+ +
@@ -733,110 +733,98 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` className="euiFlexItem euiFlexItem--flexGrowZero" >
+ + Style options +
-
- - -
-
+ + -
-
+ + -
+ +
@@ -844,110 +832,122 @@ exports[`Storyshots components/TextStylePicker interactive 1`] = ` className="euiFlexItem euiFlexItem--flexGrowZero" >
+ + Alignment options +
-
- - -
-
+ +
-
+ +
+ +
diff --git a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx index c501e78a5e338..7371eacd92fd1 100644 --- a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx +++ b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx @@ -162,6 +162,7 @@ export const TextStylePicker: FC = ({ type="multi" isIconOnly className="canvasSidebar__buttonGroup" + legend={strings.getStyleOptionsControlLegend()} /> @@ -172,6 +173,7 @@ export const TextStylePicker: FC = ({ idSelected={align} onChange={(optionId: string) => doChange('align', optionId)} className="canvasSidebar__buttonGroup" + legend={strings.getAlignmentOptionsControlLegend()} /> diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot index f1fac7e6ce477..5c9e0b6224126 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/delete_var.stories.storyshot @@ -64,6 +64,11 @@ Array [ - -
+ +
+ + @@ -307,6 +315,11 @@ Array [ className="euiButton euiButton--secondary euiButton--small euiButton--fill" disabled={false} onClick={[Function]} + style={ + Object { + "minWidth": undefined, + } + } type="button" > = ({ type, value, onChange }) => { }} buttonSize="compressed" isFullWidth + legend={strings.getBooleanOptionsLegend()} /> ); } diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot index 6bce2be335b78..de78772bcb124 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot @@ -14,9 +14,14 @@ exports[`Storyshots components/WorkpadHeader/ElementMenu default 1`] = ` >
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
"`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
"`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
"`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
"`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot index 73d350a9c1ee1..7d7b2a0ae9341 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot @@ -1445,7 +1445,7 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="euiFlexItem euiFlexItem--flexGrowZero" >
App renders properly 1`] = `
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot index 2fe222b4238a3..28c01c3fba8e5 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/__stories__/__snapshots__/footer.stories.storyshot @@ -1398,7 +1398,7 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="euiFlexItem euiFlexItem--flexGrowZero" >
can navigate Autoplay Settings 1`] = `
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/settings.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/settings.tsx index 4d827da0c3639..4e67b1c67a27f 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/settings.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/settings.tsx @@ -81,7 +81,6 @@ export const SettingsComponent: FC = ({ refs }) => { isOpen={isPopoverOpen} button={button} panelPaddingSize="none" - withTitle anchorPosition="upRight" insert={ refs.stage.current ? { sibling: refs.stage.current, position: 'after' } : undefined diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx index ce26e4e71a5db..7edb6276b8158 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx @@ -143,7 +143,6 @@ const AutoFollowPatternActionMenuUI: FunctionComponent = ({ closePopover={() => setShowPopover(false)} button={button} panelPaddingSize="none" - withTitle repositionOnScroll > diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts index d0c597532f6ed..e1d8a2b3671a2 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts @@ -12,8 +12,9 @@ import { } from '../../../../../../../src/plugins/ui_actions/public'; /** - * We know that VALUE_CLICK_TRIGGER and SELECT_RANGE_TRIGGER are also triggering APPLY_FILTER_TRIGGER - * This function appends APPLY_FILTER_TRIGGER to list of triggers if VALUE_CLICK_TRIGGER or SELECT_RANGE_TRIGGER + * We know that VALUE_CLICK_TRIGGER and SELECT_RANGE_TRIGGER are also triggering APPLY_FILTER_TRIGGER. + * This function appends APPLY_FILTER_TRIGGER to the list of triggers if either VALUE_CLICK_TRIGGER + * or SELECT_RANGE_TRIGGER was executed. * * TODO: this probably should be part of uiActions infrastructure, * but dynamic implementation of nested trigger doesn't allow to statically express such relations diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx index d5547ff8097cc..ff54e0812975d 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx @@ -129,7 +129,7 @@ describe('isCompatible', () => { }); }); - test('not compatible if no triggers intersection', async () => { + test('not compatible if no triggers intersect', async () => { await assertNonCompatibility({ actionFactoriesTriggers: [], }); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index a2192808c2d40..a417deb47db53 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -10,9 +10,12 @@ import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/pub import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { isEnhancedEmbeddable, - embeddableEnhancedContextMenuDrilldownGrouping, + embeddableEnhancedDrilldownGrouping, } from '../../../../../../embeddable_enhanced/public'; -import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public'; +import { + CONTEXT_MENU_TRIGGER, + EmbeddableContext, +} from '../../../../../../../../src/plugins/embeddable/public'; import { StartDependencies } from '../../../../plugin'; import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; import { ensureNestedTriggers } from '../drilldown_shared'; @@ -27,7 +30,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType handle.close()} viewMode={'create'} dynamicActionManager={embeddable.enhancements.dynamicActions} - triggers={ensureNestedTriggers(embeddable.supportedTriggers())} + triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} /> ), diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 56ef25005078b..1f0570445a8fc 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -10,12 +10,16 @@ import { reactToUiComponent, toMountPoint, } from '../../../../../../../../src/plugins/kibana_react/public'; -import { EmbeddableContext, ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; +import { + EmbeddableContext, + ViewMode, + CONTEXT_MENU_TRIGGER, +} from '../../../../../../../../src/plugins/embeddable/public'; import { txtDisplayName } from './i18n'; import { MenuItem } from './menu_item'; import { isEnhancedEmbeddable, - embeddableEnhancedContextMenuDrilldownGrouping, + embeddableEnhancedDrilldownGrouping, } from '../../../../../../embeddable_enhanced/public'; import { StartDependencies } from '../../../../plugin'; import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; @@ -31,7 +35,7 @@ export class FlyoutEditDrilldownAction implements ActionByType handle.close()} viewMode={'manage'} dynamicActionManager={embeddable.enhancements.dynamicActions} - triggers={ensureNestedTriggers(embeddable.supportedTriggers())} + triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} /> ), diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index aee32a7c62759..3226ecf6f0dda 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -64,7 +64,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { abortSignal: options.abortSignal, timeout: this.searchTimeout, }); - const aborted$ = from(toPromise(combinedSignal)); + const abortedPromise = toPromise(combinedSignal); const strategy = options?.strategy || ENHANCED_ES_SEARCH_STRATEGY; this.pendingCount$.next(this.pendingCount$.getValue() + 1); @@ -90,7 +90,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { }) ); }), - takeUntil(aborted$), + takeUntil(from(abortedPromise.promise)), catchError((e: any) => { // If we haven't received the response to the initial request, including the ID, then // we don't need to send a follow-up request to delete this search. Otherwise, we @@ -103,6 +103,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); + abortedPromise.cleanup(); }) ); } diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx index 85e92d0827daa..807dfeed21d1f 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx @@ -6,7 +6,11 @@ import React from 'react'; import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; -import { ChartActionContext, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { + ChartActionContext, + CONTEXT_MENU_TRIGGER, + IEmbeddable, +} from '../../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; import { SELECT_RANGE_TRIGGER, @@ -34,7 +38,10 @@ interface UrlDrilldownDeps { export type ActionContext = ChartActionContext; export type Config = UrlDrilldownConfig; -export type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER; +export type UrlTrigger = + | typeof CONTEXT_MENU_TRIGGER + | typeof VALUE_CLICK_TRIGGER + | typeof SELECT_RANGE_TRIGGER; export interface ActionFactoryContext extends BaseActionFactoryContext { embeddable?: IEmbeddable; } @@ -58,7 +65,7 @@ export class UrlDrilldown implements Drilldown = ({ diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts index 6989819da2b0b..a93e150deee8f 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts @@ -87,25 +87,25 @@ describe('VALUE_CLICK_TRIGGER', () => { ]) as ValueClickTriggerEventScope; expect(mockEventScope.points.length).toBeGreaterThan(3); expect(mockEventScope.points).toMatchInlineSnapshot(` - Array [ - Object { - "key": "event.points.0.key", - "value": "event.points.0.value", - }, - Object { - "key": "event.points.1.key", - "value": "event.points.1.value", - }, - Object { - "key": "event.points.2.key", - "value": "event.points.2.value", - }, - Object { - "key": "event.points.3.key", - "value": "event.points.3.value", - }, - ] - `); + Array [ + Object { + "key": "event.points.0.key", + "value": "event.points.0.value", + }, + Object { + "key": "event.points.1.key", + "value": "event.points.1.value", + }, + Object { + "key": "event.points.2.key", + "value": "event.points.2.value", + }, + Object { + "key": "event.points.3.key", + "value": "event.points.3.value", + }, + ] + `); }); }); @@ -130,3 +130,12 @@ describe('VALUE_CLICK_TRIGGER', () => { }); }); }); + +describe('CONTEXT_MENU_TRIGGER', () => { + test('getMockEventScope() results in empty scope', () => { + const mockEventScope = getMockEventScope([ + 'CONTEXT_MENU_TRIGGER', + ]) as ValueClickTriggerEventScope; + expect(mockEventScope).toEqual({}); + }); +}); diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts index 0f66cb144c967..234af380689e9 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts @@ -14,11 +14,15 @@ import { IEmbeddable, isRangeSelectTriggerContext, isValueClickTriggerContext, + isContextMenuTriggerContext, RangeSelectContext, ValueClickContext, } from '../../../../../../src/plugins/embeddable/public'; import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown'; -import { SELECT_RANGE_TRIGGER } from '../../../../../../src/plugins/ui_actions/public'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../../src/plugins/ui_actions/public'; type ContextScopeInput = ActionContext | ActionFactoryContext; @@ -101,7 +105,10 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld * URL drilldown event scope, * available as {{event.$}} */ -export type UrlDrilldownEventScope = ValueClickTriggerEventScope | RangeSelectTriggerEventScope; +export type UrlDrilldownEventScope = + | ValueClickTriggerEventScope + | RangeSelectTriggerEventScope + | ContextMenuTriggerEventScope; export type EventScopeInput = ActionContext; export interface ValueClickTriggerEventScope { key?: string; @@ -115,11 +122,15 @@ export interface RangeSelectTriggerEventScope { to?: string | number; } +export type ContextMenuTriggerEventScope = object; + export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEventScope { if (isRangeSelectTriggerContext(eventScopeInput)) { return getEventScopeFromRangeSelectTriggerContext(eventScopeInput); } else if (isValueClickTriggerContext(eventScopeInput)) { return getEventScopeFromValueClickTriggerContext(eventScopeInput); + } else if (isContextMenuTriggerContext(eventScopeInput)) { + return {}; } else { throw new Error("UrlDrilldown [getEventScope] can't build scope from not supported trigger"); } @@ -169,7 +180,9 @@ export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventSco from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago to: new Date().toISOString(), }; - } else { + } + + if (trigger === VALUE_CLICK_TRIGGER) { // number of mock points to generate // should be larger or equal of any possible data points length emitted by VALUE_CLICK_TRIGGER const nPoints = 4; @@ -184,6 +197,8 @@ export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventSco points, }; } + + return {}; } type Primitive = string | number | boolean | null; diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts b/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts index 5ea8928532c28..0aa1c0e6f08ae 100644 --- a/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts +++ b/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; import { UiActionsPresentableGrouping as PresentableGrouping } from '../../../../../src/plugins/ui_actions/public'; -export const contextMenuDrilldownGrouping: PresentableGrouping<{ +export const drilldownGrouping: PresentableGrouping<{ embeddable?: IEmbeddable; }> = [ { id: 'drilldowns', - getDisplayName: () => 'Drilldowns', + getDisplayName: () => + i18n.translate('xpack.embeddableEnhanced.Drilldowns', { + defaultMessage: 'Drilldowns', + }), getIconType: () => 'symlink', order: 25, }, diff --git a/x-pack/plugins/embeddable_enhanced/public/index.ts b/x-pack/plugins/embeddable_enhanced/public/index.ts index a7916685239df..24f8eb623abe0 100644 --- a/x-pack/plugins/embeddable_enhanced/public/index.ts +++ b/x-pack/plugins/embeddable_enhanced/public/index.ts @@ -20,4 +20,4 @@ export function plugin(context: PluginInitializerContext) { export { EnhancedEmbeddable, EnhancedEmbeddableContext } from './types'; export { isEnhancedEmbeddable } from './embeddables'; -export { contextMenuDrilldownGrouping as embeddableEnhancedContextMenuDrilldownGrouping } from './actions'; +export { drilldownGrouping as embeddableEnhancedDrilldownGrouping } from './actions'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts index ced4dda48fcd2..acc2d2247efcb 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts @@ -57,24 +57,24 @@ describe('Key rotation routes', () => { const queryValidator = (routeConfig.validate as any).query as Type; expect( queryValidator.validate({ - batchSize: 100, + batch_size: 100, type: 'some-type', }) ).toEqual({ - batchSize: 100, + batch_size: 100, type: 'some-type', }); - expect(queryValidator.validate({ batchSize: 1 })).toEqual({ batchSize: 1 }); - expect(queryValidator.validate({ batchSize: 10000 })).toEqual({ batchSize: 10000 }); - expect(queryValidator.validate({})).toEqual({ batchSize: 10000 }); + expect(queryValidator.validate({ batch_size: 1 })).toEqual({ batch_size: 1 }); + expect(queryValidator.validate({ batch_size: 10000 })).toEqual({ batch_size: 10000 }); + expect(queryValidator.validate({})).toEqual({ batch_size: 10000 }); - expect(() => queryValidator.validate({ batchSize: 0 })).toThrowErrorMatchingInlineSnapshot( - `"[batchSize]: Value must be equal to or greater than [1]."` + expect(() => queryValidator.validate({ batch_size: 0 })).toThrowErrorMatchingInlineSnapshot( + `"[batch_size]: Value must be equal to or greater than [1]."` ); expect(() => - queryValidator.validate({ batchSize: 10001 }) + queryValidator.validate({ batch_size: 10001 }) ).toThrowErrorMatchingInlineSnapshot( - `"[batchSize]: Value must be equal to or lower than [10000]."` + `"[batch_size]: Value must be equal to or lower than [10000]."` ); expect(() => queryValidator.validate({ type: 100 })).toThrowErrorMatchingInlineSnapshot( @@ -106,7 +106,7 @@ describe('Key rotation routes', () => { const unhandledException = new Error('Something went wrong.'); mockEncryptionKeyRotationService.rotate.mockRejectedValue(unhandledException); - const mockRequest = httpServerMock.createKibanaRequest({ query: { batchSize: 1234 } }); + const mockRequest = httpServerMock.createKibanaRequest({ query: { batch_size: 1234 } }); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(500); @@ -117,7 +117,7 @@ describe('Key rotation routes', () => { }); it('returns whatever `rotate` returns.', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ query: { batchSize: 1234 } }); + const mockRequest = httpServerMock.createKibanaRequest({ query: { batch_size: 1234 } }); mockEncryptionKeyRotationService.rotate.mockResolvedValue({ total: 3, successful: 6, @@ -132,7 +132,7 @@ describe('Key rotation routes', () => { }); it('returns 429 if called while rotation is in progress.', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ query: { batchSize: 1234 } }); + const mockRequest = httpServerMock.createKibanaRequest({ query: { batch_size: 1234 } }); mockEncryptionKeyRotationService.rotate.mockResolvedValue({ total: 3, successful: 6, diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts index 48b29387106ee..c74f67220472c 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts @@ -28,7 +28,7 @@ export function defineKeyRotationRoutes({ path: '/api/encrypted_saved_objects/_rotate_key', validate: { query: schema.object({ - batchSize: schema.number({ + batch_size: schema.number({ min: 1, max: DEFAULT_MAX_RESULT_WINDOW, defaultValue: DEFAULT_MAX_RESULT_WINDOW, @@ -60,7 +60,7 @@ export function defineKeyRotationRoutes({ try { return response.ok({ body: await encryptionKeyRotationService.rotate(request, { - batchSize: request.query.batchSize, + batchSize: request.query.batch_size, type: request.query.type, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx index e5fdcc3089059..c9bf03c5d4700 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx @@ -45,7 +45,6 @@ export const FilterableUsersPopover: React.FC = ( isOpen={isPopoverOpen} closePopover={closePopover} panelPaddingSize="none" - withTitle={true} > = isOpen={isPopoverOpen} closePopover={closePopover} panelPaddingSize="none" - withTitle={true} > {contentSourceCountHeading}
{sources}
diff --git a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx index f4006d6bf142b..ec5da1f44223c 100644 --- a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx +++ b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx @@ -116,7 +116,7 @@ export function FieldEditor({ return (
= ({ isOpen={isPolicyPopoverOpen(policy.name)} closePopover={closePolicyPopover} panelPaddingSize="none" - withTitle anchorPosition="rightUp" repositionOnScroll > diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx index d711863c309e9..afdf726ea02f9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx @@ -158,7 +158,6 @@ export class IndexLifecycleSummary extends Component { button={button} isOpen={this.state.showPhaseExecutionPopover} closePopover={this.closePhaseExecutionPopover} - withTitle > = ({ isOpen={isPopoverOpen} closePopover={() => setIsPopOverOpen(false)} panelPaddingSize="none" - withTitle anchorPosition="rightUp" repositionOnScroll > diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/create_button_popover.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/create_button_popover.tsx index 941e8ec362de2..8939e58ffd94f 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/create_button_popover.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/components/create_button_popover.tsx @@ -36,7 +36,6 @@ export const CreateButtonPopOver = ({ anchorPosition = 'upCenter' }: Props) => { isOpen={isPopoverOpen} closePopover={() => setIsPopOverOpen(false)} panelPaddingSize="none" - withTitle anchorPosition={anchorPosition} repositionOnScroll > diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 6e96ef56d683f..cf6fcfc238e06 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -746,7 +746,6 @@ export class IndexActionsContextMenu extends Component { isOpen={this.state.isPopoverOpen} closePopover={this.closePopover} panelPaddingSize="none" - withTitle anchorPosition={anchorPosition} repositionOnScroll > diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx index 4899c5c664eba..1b1b9ad013c37 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -262,7 +262,6 @@ export const TemplateDetailsContent = ({ isOpen={isPopoverOpen} closePopover={() => setIsPopOverOpen(false)} panelPaddingSize="none" - withTitle anchorPosition="rightUp" repositionOnScroll > diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index 02c3ea29c1846..7b15acfb6c4e2 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -127,7 +127,7 @@ export const AlertPreview: React.FC = (props) => { defaultMessage: 'Preview', })} fullWidth - compressed + display="rowCompressed" > <> diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index f47f30c280b2a..66d547eb50d9c 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -365,7 +365,7 @@ export const Expressions: React.FC = (props) => { defaultMessage: 'Use a KQL expression to limit the scope of your alert trigger.', })} fullWidth - compressed + display="rowCompressed" > {(alertsContext.metadata && ( { setAggFieldPopoverOpen(false); }} - withTitle anchorPosition={popupPosition ?? 'downRight'} zIndex={8000} > diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx index 9c215b89f4634..4dca479271832 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx @@ -61,7 +61,6 @@ export const NodeTypeExpression = ({ setAggTypePopoverOpen(false); }} ownFocus - withTitle anchorPosition={popupPosition ?? 'downLeft'} >
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index c71a3b6b13338..92c0172703423 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -364,7 +364,7 @@ export const Expressions: React.FC = (props) => { defaultMessage: 'Use a KQL expression to limit the scope of your alert trigger.', })} fullWidth - compressed + display="rowCompressed" > {(alertsContext.metadata && ( = (props) => { 'Create an alert for every unique value. For example: "host.id" or "cloud.region".', })} fullWidth - compressed + display="rowCompressed" > = ({ fill aria-label={ariaLabel || DEFAULT_MENU_LABEL} onClick={onOpen} - style={{ minWidth: 'auto' }} + minWidth="auto" > diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx index 92b21d676c9bb..e270b5650854d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx @@ -227,7 +227,7 @@ export const JobSetupScreen = (props: Props) => { defaultMessage="Partition field" /> } - compressed + display="rowCompressed" > Legend Options diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx index c88446eaf3f6a..28f13d582082b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx @@ -135,7 +135,6 @@ export const WaffleInventorySwitcher: React.FC = () => { isOpen={isOpen} closePopover={closePopover} panelPaddingSize="none" - withTitle anchorPosition="downLeft" > diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx index d21454dba5178..c6344e03d2cfd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx @@ -121,7 +121,7 @@ export const MetricsExplorerChartOptions = ({ chartOptions, onChange }: Props) = > isPipeline(path)); // get and save pipeline refs before installing pipelines @@ -50,6 +53,20 @@ export const installPipelines = async ( acc.push(...pipelineObjectRefs); return acc; }, []); + + // check that we don't duplicate the pipeline refs if the user is reinstalling + const installedPkg = await getInstallationObject({ + savedObjectsClient, + pkgName, + }); + if (!installedPkg) throw new Error("integration wasn't found while installing pipelines"); + // remove the current pipeline refs, if any exist, associated with this version before saving new ones so no duplicates occur + await deletePipelineRefs( + savedObjectsClient, + installedPkg.attributes.installed_es, + pkgName, + pkgVersion + ); await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, pipelineRefs); const pipelines = dataStreams.reduce>>((acc, dataStream) => { if (dataStream.ingest_pipeline) { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx index 72c25d6dff72d..135a5854f36cd 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -105,7 +105,7 @@ const createActions = (testBed: TestBed) => { moveProcessor(processorSelector: string, dropZoneSelector: string) { act(() => { - find(`${processorSelector}.moveItemButton`).simulate('change'); + find(`${processorSelector}.moveItemButton`).simulate('click'); }); component.update(); act(() => { @@ -137,11 +137,11 @@ const createActions = (testBed: TestBed) => { startAndCancelMove(processorSelector: string) { act(() => { - find(`${processorSelector}.moveItemButton`).simulate('change'); + find(`${processorSelector}.moveItemButton`).simulate('click'); }); component.update(); act(() => { - find(`${processorSelector}.cancelMoveItemButton`).simulate('change'); + find(`${processorSelector}.cancelMoveItemButton`).simulate('click'); }); component.update(); }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index dd7798a37dd4e..707f6e7f1e2c9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -7,7 +7,7 @@ import classNames from 'classnames'; import React, { FunctionComponent, memo, useCallback } from 'react'; import { - EuiButtonToggle, + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiLink, @@ -127,17 +127,14 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( const icon = isMovingThisProcessor ? 'cross' : 'sortable'; const disabled = isEditorNotInIdleMode && !isMovingThisProcessor; const moveButton = ( - { + onClick={() => { if (isMovingThisProcessor) { onCancelMove(); } else { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx index 19c3c49396c6e..6d0b2a1dcb2cf 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx @@ -78,7 +78,6 @@ export const DocumentsDropdown: FunctionComponent = ({ closePopover={() => setShowPopover(false)} button={managePipelineButton} panelPaddingSize="none" - withTitle repositionOnScroll data-test-subj="documentsDropdown" panelClassName="documentsDropdownPanel" diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx index 3122aa6da7396..bbb7603c53967 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx @@ -187,7 +187,6 @@ export const PipelineDetailsFlyout: FunctionComponent = ({ closePopover={() => setShowPopover(false)} button={managePipelineButton} panelPaddingSize="none" - withTitle repositionOnScroll >
- {(!emptyExpression || title) && ( + {!emptyExpression || title ? ( - +

{title || i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} - +

+ ) : ( + +

+ {title || + i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} +

+
)} {children} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 2fbe23f9085f2..7f744d1dc3cbc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -325,7 +325,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { id: 'histogram', }, ]} - onChange={(optionId) => { + onChange={(optionId: string) => { setShowingHistogram(optionId === 'histogram'); }} idSelected={showingHistogram ? 'histogram' : 'topValues'} diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 2767642254322..72c04992566bd 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -589,6 +589,7 @@ exports[`UploadLicense should display a modal when license requires acknowledgem onClick={[Function]} > ) { const colorForm = props.styleProperty.isDynamic() ? ( ) : ( @@ -19,7 +22,7 @@ export function VectorStyleColorEditor(props) { ); return ( - {...props} customStaticOptionLabel={i18n.translate( 'xpack.maps.styles.color.staticDynamicSelect.staticLabel', diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx similarity index 56% rename from x-pack/plugins/maps/public/classes/styles/vector/components/field_select.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx index dcc1f1eadbd54..57c63413aecda 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.tsx @@ -4,20 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import PropTypes from 'prop-types'; import React from 'react'; -import { EuiComboBox, EuiHighlight, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { FIELD_ORIGIN } from '../../../../../common/constants'; +import { + EuiComboBox, + EuiComboBoxProps, + EuiComboBoxOptionOption, + EuiHighlight, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FIELD_ORIGIN, VECTOR_STYLES } from '../../../../../common/constants'; import { FieldIcon } from '../../../../../../../../src/plugins/kibana_react/public'; +import { StyleField } from '../style_fields_helper'; -function renderOption(option, searchValue, contentClassName) { +function renderOption( + option: EuiComboBoxOptionOption, + searchValue: string, + contentClassName: string +) { + const fieldIcon = option.value ? : null; return ( - - - + {fieldIcon} {option.label} @@ -25,11 +35,11 @@ function renderOption(option, searchValue, contentClassName) { ); } -function groupFieldsByOrigin(fields) { - const fieldsByOriginMap = new Map(); +function groupFieldsByOrigin(fields: StyleField[]) { + const fieldsByOriginMap = new Map(); fields.forEach((field) => { if (fieldsByOriginMap.has(field.origin)) { - const fieldsList = fieldsByOriginMap.get(field.origin); + const fieldsList = fieldsByOriginMap.get(field.origin)!; fieldsList.push(field); fieldsByOriginMap.set(field.origin, fieldsList); } else { @@ -37,7 +47,7 @@ function groupFieldsByOrigin(fields) { } }); - function fieldsListToOptions(fieldsList) { + function fieldsListToOptions(fieldsList: StyleField[]) { return fieldsList .map((field) => { return { value: field, label: field.label }; @@ -50,11 +60,14 @@ function groupFieldsByOrigin(fields) { if (fieldsByOriginMap.size === 1) { // do not show origin group if all fields are from same origin const onlyOriginKey = fieldsByOriginMap.keys().next().value; - const fieldsList = fieldsByOriginMap.get(onlyOriginKey); + const fieldsList = fieldsByOriginMap.get(onlyOriginKey)!; return fieldsListToOptions(fieldsList); } - const optionGroups = []; + const optionGroups: Array<{ + label: string; + options: Array>; + }> = []; fieldsByOriginMap.forEach((fieldsList, fieldOrigin) => { optionGroups.push({ label: i18n.translate('xpack.maps.style.fieldSelect.OriginLabel', { @@ -65,29 +78,46 @@ function groupFieldsByOrigin(fields) { }); }); - optionGroups.sort((a, b) => { - return a.label.toLowerCase().localeCompare(b.label.toLowerCase()); - }); + optionGroups.sort( + (a: EuiComboBoxOptionOption, b: EuiComboBoxOptionOption) => { + return a.label.toLowerCase().localeCompare(b.label.toLowerCase()); + } + ); return optionGroups; } -export function FieldSelect({ fields, selectedFieldName, onChange, styleName, ...rest }) { - const onFieldChange = (selectedFields) => { +type Props = { + fields: StyleField[]; + selectedFieldName: string; + onChange: ({ field }: { field: StyleField | null }) => void; + styleName: VECTOR_STYLES; +} & Omit< + EuiComboBoxProps, + | 'selectedOptions' + | 'options' + | 'onChange' + | 'singleSelection' + | 'isClearable' + | 'fullWidth' + | 'renderOption' +>; + +export function FieldSelect({ fields, selectedFieldName, onChange, styleName, ...rest }: Props) { + const onFieldChange = (selectedFields: Array>) => { onChange({ - field: selectedFields.length > 0 ? selectedFields[0].value : null, + field: selectedFields.length > 0 && selectedFields[0].value ? selectedFields[0].value : null, }); }; let selectedOption; if (selectedFieldName) { - const field = fields.find((field) => { - return field.name === selectedFieldName; + const field = fields.find((f) => { + return f.name === selectedFieldName; }); - //Do not spread in all the other unused values (e.g. type, supportsAutoDomain etc...) if (field) { selectedOption = { - value: field.value, + value: field, label: field.label, }; } @@ -110,15 +140,3 @@ export function FieldSelect({ fields, selectedFieldName, onChange, styleName, .. /> ); } - -export const fieldShape = PropTypes.shape({ - name: PropTypes.string.isRequired, - origin: PropTypes.oneOf(Object.values(FIELD_ORIGIN)).isRequired, - type: PropTypes.string.isRequired, -}); - -FieldSelect.propTypes = { - selectedFieldName: PropTypes.string, - fields: PropTypes.arrayOf(fieldShape).isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.js b/x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.ts similarity index 94% rename from x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.ts index 5d39b423e56e6..1b5f3f47d2204 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.ts @@ -8,14 +8,14 @@ import { i18n } from '@kbn/i18n'; import { VECTOR_STYLES } from '../../../../../common/constants'; -export function getDisabledByMessage(styleName) { +export function getDisabledByMessage(styleName: VECTOR_STYLES) { return i18n.translate('xpack.maps.styles.vector.disabledByMessage', { defaultMessage: `Set '{styleLabel}' to enable`, values: { styleLabel: getVectorStyleLabel(styleName) }, }); } -export function getVectorStyleLabel(styleName) { +export function getVectorStyleLabel(styleName: VECTOR_STYLES) { switch (styleName) { case VECTOR_STYLES.FILL_COLOR: return i18n.translate('xpack.maps.styles.vector.fillColorLabel', { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/stop_input.js b/x-pack/plugins/maps/public/classes/styles/vector/components/stop_input.tsx similarity index 76% rename from x-pack/plugins/maps/public/classes/styles/vector/components/stop_input.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/stop_input.tsx index 64a5f806e34c4..765626329a7c5 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/stop_input.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/stop_input.tsx @@ -5,18 +5,37 @@ */ import _ from 'lodash'; -import React, { Component } from 'react'; +import React, { ChangeEvent, Component } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFieldText } from '@elastic/eui'; +import { IField } from '../../../fields/field'; + +interface Props { + dataTestSubj: string; + field: IField; + getValueSuggestions: (query: string) => Promise; + onChange: (value: string) => void; + value: string; +} + +interface State { + suggestions: string[]; + isLoadingSuggestions: boolean; + hasPrevFocus: boolean; + fieldDataType: string | null; + localFieldTextValue: string; + searchValue?: string; +} -import { EuiComboBox, EuiFieldText } from '@elastic/eui'; +export class StopInput extends Component { + private _isMounted: boolean = false; -export class StopInput extends Component { - constructor(props) { + constructor(props: Props) { super(props); this.state = { suggestions: [], isLoadingSuggestions: false, hasPrevFocus: false, - fieldDataType: undefined, + fieldDataType: null, localFieldTextValue: props.value, }; } @@ -45,15 +64,15 @@ export class StopInput extends Component { } }; - _onChange = (selectedOptions) => { + _onChange = (selectedOptions: Array>) => { this.props.onChange(_.get(selectedOptions, '[0].label', '')); }; - _onCreateOption = (newValue) => { + _onCreateOption = (newValue: string) => { this.props.onChange(newValue); }; - _onSearchChange = async (searchValue) => { + _onSearchChange = async (searchValue: string) => { this.setState( { isLoadingSuggestions: true, @@ -65,8 +84,8 @@ export class StopInput extends Component { ); }; - _loadSuggestions = _.debounce(async (searchValue) => { - let suggestions = []; + _loadSuggestions = _.debounce(async (searchValue: string) => { + let suggestions: string[] = []; try { suggestions = await this.props.getValueSuggestions(searchValue); } catch (error) { @@ -81,7 +100,7 @@ export class StopInput extends Component { } }, 300); - _onFieldTextChange = (event) => { + _onFieldTextChange = (event: ChangeEvent) => { this.setState({ localFieldTextValue: event.target.value }); // onChange can cause UI lag, ensure smooth input typing by debouncing onChange this._debouncedOnFieldTextChange(); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx similarity index 59% rename from x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx index e7df47bc6d4cb..43b088074a30e 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment } from 'react'; -import { getVectorStyleLabel, getDisabledByMessage } from './get_vector_style_label'; +import React, { Component, Fragment, ReactElement } from 'react'; import { EuiFormRow, EuiSelect, @@ -14,12 +13,31 @@ import { EuiFieldText, EuiToolTip, } from '@elastic/eui'; -import { STYLE_TYPE } from '../../../../../common/constants'; import { i18n } from '@kbn/i18n'; +import { getVectorStyleLabel, getDisabledByMessage } from './get_vector_style_label'; +import { STYLE_TYPE, VECTOR_STYLES } from '../../../../../common/constants'; +import { FieldMetaOptions } from '../../../../../common/descriptor_types'; +import { IStyleProperty } from '../properties/style_property'; +import { StyleField } from '../style_fields_helper'; + +export interface Props { + children: ReactElement; + customStaticOptionLabel?: string; + defaultStaticStyleOptions: StaticOptions; + defaultDynamicStyleOptions: DynamicOptions; + disabled: boolean; + disabledBy?: VECTOR_STYLES; + fields: StyleField[]; + onDynamicStyleChange: (propertyName: VECTOR_STYLES, options: DynamicOptions) => void; + onStaticStyleChange: (propertyName: VECTOR_STYLES, options: StaticOptions) => void; + styleProperty: IStyleProperty; +} -export class StylePropEditor extends Component { - _prevStaticStyleOptions = this.props.defaultStaticStyleOptions; - _prevDynamicStyleOptions = this.props.defaultDynamicStyleOptions; +export class StylePropEditor extends Component< + Props +> { + private _prevStaticStyleOptions = this.props.defaultStaticStyleOptions; + private _prevDynamicStyleOptions = this.props.defaultDynamicStyleOptions; _onTypeToggle = () => { if (this.props.styleProperty.isDynamic()) { @@ -41,7 +59,7 @@ export class StylePropEditor extends Component { } }; - _onFieldMetaOptionsChange = (fieldMetaOptions) => { + _onFieldMetaOptionsChange = (fieldMetaOptions: FieldMetaOptions) => { const options = { ...this.props.styleProperty.getOptions(), fieldMetaOptions, @@ -89,28 +107,29 @@ export class StylePropEditor extends Component { const staticDynamicSelect = this.renderStaticDynamicSelect(); - const stylePropertyForm = this.props.disabled ? ( - - - - {staticDynamicSelect} - - - - - - - ) : ( - - {React.cloneElement(this.props.children, { - staticDynamicSelect, - })} - {fieldMetaOptionsPopover} - - ); + const stylePropertyForm = + this.props.disabled && this.props.disabledBy ? ( + + + + {staticDynamicSelect} + + + + + + + ) : ( + + {React.cloneElement(this.props.children, { + staticDynamicSelect, + })} + {fieldMetaOptionsPopover} + + ); return ( + ); diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index 60f8c8005fe4c..c6c784481436c 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -175,7 +175,6 @@ export class JoinExpression extends Component { closePopover={this._closePopover} ownFocus initialFocus="body" /* avoid initialFocus on Combobox */ - withTitle anchorPosition="leftCenter" button={ { isOpen={this.state.isPopoverOpen} closePopover={this._closePopover} panelPaddingSize="none" - withTitle anchorPosition="leftUp" > diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap index 0f620bdeb1c0e..456414889c732 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -57,7 +57,6 @@ exports[`TOCEntryActionsPopover is rendered 1`] = ` isOpen={false} ownFocus={false} panelPaddingSize="none" - withTitle={true} > { isOpen={this.state.isPopoverOpen} closePopover={this._closePopover} panelPaddingSize="none" - withTitle anchorPosition="leftUp" anchorClassName="mapLayTocActions__popoverAnchor" > diff --git a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx index d54a7fe81e858..449c017d533cc 100644 --- a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx +++ b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx @@ -37,7 +37,9 @@ describe('AnomalyResultsViewSelector', () => { // Check the Single Metric Viewer element exists in the selector, and that it is checked. expect(getByTestId('mlAnomalyResultsViewSelectorSingleMetricViewer')).toBeInTheDocument(); expect( - getByTestId('mlAnomalyResultsViewSelectorSingleMetricViewer').hasAttribute('checked') + getByTestId('mlAnomalyResultsViewSelectorSingleMetricViewer') + .querySelector('input')! + .hasAttribute('checked') ).toBe(true); }); }); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap b/x-pack/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap index 43b4625e81f79..4ea930d74d3d6 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap @@ -30,7 +30,6 @@ exports[`ConditionExpression renders with appliesTo, operator and value supplied isOpen={false} ownFocus={true} panelPaddingSize="none" - withTitle={true} >
{this.renderAppliesToPopover()} diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_expression.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_expression.js index a8c7ac0f6f598..ced81d59ab51e 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_expression.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_expression.js @@ -157,7 +157,6 @@ export class ScopeExpression extends Component { closePopover={this.closeFilterList} panelPaddingSize="none" ownFocus - withTitle anchorPosition="downLeft" > {this.renderFilterListPopover()} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index c837fcbacdd55..4e84bd5ffeddb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -7,7 +7,6 @@ import React, { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'; import { EuiButtonGroup, EuiCode, EuiFlexGroup, EuiFlexItem, EuiInputPopover } from '@elastic/eui'; -import { EuiButtonGroupIdToSelectedMap } from '@elastic/eui/src/components/button/button_group/button_group'; import { i18n } from '@kbn/i18n'; @@ -54,7 +53,7 @@ export const ExplorationQueryBar: FC = ({ query: '', language: SEARCH_QUERY_LANGUAGE.KUERY, }); - const [idToSelectedMap, setIdToSelectedMap] = useState({}); + const [idToSelectedMap, setIdToSelectedMap] = useState<{ [id: string]: boolean }>({}); const [errorMessage, setErrorMessage] = useState(undefined); @@ -174,11 +173,10 @@ export const ExplorationQueryBar: FC = ({ defaultMessage: 'Analytics query bar filter buttons', } )} - name="analyticsQueryBarFilterButtons" options={filters.options} type="multi" idToSelectedMap={idToSelectedMap} - onChange={(optionId) => { + onChange={(optionId: string) => { const newIdToSelectedMap = { [optionId]: !idToSelectedMap[optionId] }; setIdToSelectedMap(newIdToSelectedMap); handleFilterUpdate(optionId, newIdToSelectedMap); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx index e2fb8ae5547cc..bfc431166fab9 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx @@ -146,7 +146,7 @@ export const NumberContent: FC = ({ config }) => { options={detailsOptions} idSelected={detailsMode} onChange={(optionId) => setDetailsMode(optionId as DETAILS_MODE)} - aria-label={i18n.translate( + legend={i18n.translate( 'xpack.ml.fieldDataCard.cardNumber.selectMetricDetailsDisplayAriaLabel', { defaultMessage: 'Select display option for metric details', diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap index 06ee16f264756..7d5c73b42f15b 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap @@ -21,9 +21,8 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe > = ({ className="url-label" error={invalidLabelError} isInvalid={isInvalidLabel} - compressed + display="rowCompressed" > = ({ label={ } - compressed + display="rowCompressed" > = ({ defaultMessage="Dashboard name" /> } - compressed + display="rowCompressed" > = ({ defaultMessage="Index pattern" /> } - compressed + display="rowCompressed" > = ({ /> } className="url-time-range" - compressed + display="rowCompressed" > = ({ className="url-time-range" error={invalidIntervalError} isInvalid={isInvalidTimeRange} - compressed + display="rowCompressed" > = ({ label={ } - compressed + display="rowCompressed" fullWidth={true} > ; + alertStates: Array< + AlertState | AlertCpuUsageState | AlertDiskUsageState | AlertThreadPoolRejectionsState + >; [x: string]: unknown; } @@ -46,6 +100,13 @@ export interface AlertMemoryUsageState extends AlertNodeState { memoryUsage: number; } +export interface AlertThreadPoolRejectionsState extends AlertState { + rejectionCount: number; + type: string; + nodeId: string; + nodeName?: string; +} + export interface AlertUiState { isFiring: boolean; severity: AlertSeverity; @@ -100,6 +161,14 @@ export interface AlertCpuUsageNodeStats extends AlertNodeStats { containerQuota: number; } +export interface AlertThreadPoolRejectionsStats { + clusterUuid: string; + nodeId: string; + nodeName: string; + rejectionCount: number; + ccs?: string; +} + export interface AlertDiskUsageNodeStats extends AlertNodeStats { diskUsage: number; } @@ -121,7 +190,7 @@ export interface AlertData { instanceKey: string; clusterUuid: string; ccs?: string; - shouldFire: boolean; + shouldFire?: boolean; severity: AlertSeverity; meta: any; } diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index d4e823a194f8e..5087fe7b70c06 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -14,11 +14,11 @@ import { EuiFlexItem, EuiText, } from '@elastic/eui'; -import { CommonAlertStatus, CommonAlertState } from '../../common/types'; +import { CommonAlertStatus, CommonAlertState } from '../../common/types/alerts'; import { AlertSeverity } from '../../common/enums'; // @ts-ignore import { formatDateTimeLocal } from '../../common/formatting'; -import { AlertMessage, AlertState } from '../../server/alerts/types'; +import { AlertMessage, AlertState } from '../../common/types/alerts'; import { AlertPanel } from './panel'; import { Legacy } from '../legacy_shims'; import { isInSetupMode } from '../lib/setup_mode'; @@ -94,7 +94,6 @@ export const AlertsBadge: React.FC = (props: Props) => { isOpen={showPopover === true} closePopover={() => setShowPopover(null)} panelPaddingSize="none" - withTitle anchorPosition="downLeft" > @@ -178,7 +177,6 @@ export const AlertsBadge: React.FC = (props: Props) => { isOpen={showPopover === type} closePopover={() => setShowPopover(null)} panelPaddingSize="none" - withTitle anchorPosition="downLeft" > diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx index 1ddd41c268456..769d4dc7b256d 100644 --- a/x-pack/plugins/monitoring/public/alerts/callout.tsx +++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx @@ -7,10 +7,10 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { CommonAlertStatus } from '../../common/types'; +import { CommonAlertStatus } from '../../common/types/alerts'; import { AlertSeverity } from '../../common/enums'; import { replaceTokens } from './lib/replace_tokens'; -import { AlertMessage, AlertState } from '../../server/alerts/types'; +import { AlertMessage, AlertState } from '../../common/types/alerts'; const TYPES = [ { diff --git a/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx b/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx index 2df7169efc675..26593fdd6e7b0 100644 --- a/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx +++ b/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx @@ -6,10 +6,11 @@ import React, { Fragment } from 'react'; import { EuiForm, EuiSpacer } from '@elastic/eui'; -import { CommonAlertParamDetails } from '../../../../common/types'; +import { CommonAlertParamDetails } from '../../../../common/types/alerts'; import { AlertParamDuration } from '../../flyout_expressions/alert_param_duration'; import { AlertParamType } from '../../../../common/enums'; import { AlertParamPercentage } from '../../flyout_expressions/alert_param_percentage'; +import { AlertParamNumber } from '../../flyout_expressions/alert_param_number'; export interface Props { alertParams: { [property: string]: any }; @@ -26,14 +27,14 @@ export const Expression: React.FC = (props) => { const details = paramDetails[alertParamName]; const value = alertParams[alertParamName]; - switch (details.type) { + switch (details?.type) { case AlertParamType.Duration: return ( @@ -43,12 +44,23 @@ export const Expression: React.FC = (props) => { ); + case AlertParamType.Number: + return ( + + ); } }); diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index fb4ecacf57fd6..d15fe6344ec0f 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -6,20 +6,17 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { ALERT_CPU_USAGE } from '../../../common/constants'; +import { ALERT_CPU_USAGE, ALERT_DETAILS } from '../../../common/constants'; import { validate } from '../components/duration/validation'; import { Expression, Props } from '../components/duration/expression'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { CpuUsageAlert } from '../../../server/alerts'; export function createCpuUsageAlertType(): AlertTypeModel { - const alert = new CpuUsageAlert(); return { id: ALERT_CPU_USAGE, - name: alert.label, + name: ALERT_DETAILS[ALERT_CPU_USAGE].label, iconClass: 'bell', alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index c2abb35612b38..589b374cae32c 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -10,17 +10,15 @@ import { Expression, Props } from '../components/duration/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { DiskUsageAlert } from '../../../server/alerts'; +import { ALERT_DISK_USAGE, ALERT_DETAILS } from '../../../common/constants'; export function createDiskUsageAlertType(): AlertTypeModel { return { - id: DiskUsageAlert.TYPE, - name: DiskUsageAlert.LABEL, + id: ALERT_DISK_USAGE, + name: ALERT_DETAILS[ALERT_DISK_USAGE].label, iconClass: 'bell', alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts b/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts index 63714a6921e3f..e13ea7de0e226 100644 --- a/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts +++ b/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CommonAlertState, CommonAlertStatus } from '../../common/types'; +import { CommonAlertState, CommonAlertStatus } from '../../common/types/alerts'; export function filterAlertStates( alerts: { [type: string]: CommonAlertStatus }, diff --git a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx index 862f32efd7361..4ece1b0c81827 100644 --- a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx +++ b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_duration.tsx @@ -69,7 +69,7 @@ export const AlertParamDuration: React.FC = (props: Props) => { }, [unit, value]); return ( - 0}> + 0}> void; +} +export const AlertParamNumber: React.FC = (props: Props) => { + const { name, label, setAlertParams, errors } = props; + const [value, setValue] = useState(props.value); + return ( + 0}> + { + let newValue = Number(e.target.value); + if (isNaN(newValue)) { + newValue = 0; + } + setValue(newValue); + setAlertParams(name, newValue); + }} + /> + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index f6223d41ab30e..83201b0512dbb 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -3,24 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTextColor, EuiSpacer } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { LEGACY_ALERTS } from '../../../common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BY_TYPE } from '../../../server/alerts'; +import { LEGACY_ALERTS, LEGACY_ALERT_DETAILS } from '../../../common/constants'; export function createLegacyAlertTypes(): AlertTypeModel[] { return LEGACY_ALERTS.map((legacyAlert) => { - const alertCls = BY_TYPE[legacyAlert]; - const alert = new alertCls(); return { id: legacyAlert, - name: alert.label, + name: LEGACY_ALERT_DETAILS[legacyAlert].label, iconClass: 'bell', - alertParamsExpression: (props: any) => ( + alertParamsExpression: () => ( diff --git a/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx b/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx index 02f5703f66382..b8ac69cbae68a 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/replace_tokens.tsx @@ -11,7 +11,7 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertMessageDocLinkToken, -} from '../../../server/alerts/types'; +} from '../../../common/types/alerts'; // @ts-ignore import { formatTimestampToDuration } from '../../../common'; import { CALCULATE_DURATION_UNTIL } from '../../../common/constants'; diff --git a/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts index 0b95592d92c84..2ec5d1ba8f94a 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts +++ b/x-pack/plugins/monitoring/public/alerts/lib/should_show_alert_badge.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { isInSetupMode } from '../../lib/setup_mode'; -import { CommonAlertStatus } from '../../../common/types'; +import { CommonAlertStatus } from '../../../common/types/alerts'; import { ISetupModeContext } from '../../components/setup_mode/setup_mode_context'; export function shouldShowAlertBadge( diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index dd60967a3458b..d3d48d907d02e 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -10,17 +10,15 @@ import { Expression, Props } from '../components/duration/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MemoryUsageAlert } from '../../../server/alerts'; +import { ALERT_MEMORY_USAGE, ALERT_DETAILS } from '../../../common/constants'; export function createMemoryUsageAlertType(): AlertTypeModel { return { - id: MemoryUsageAlert.TYPE, - name: MemoryUsageAlert.LABEL, + id: ALERT_MEMORY_USAGE, + name: ALERT_DETAILS[ALERT_MEMORY_USAGE].label, iconClass: 'bell', alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx index 7dc6155de529e..ac30a02173a5c 100644 --- a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { EuiForm, EuiSpacer } from '@elastic/eui'; -import { CommonAlertParamDetails } from '../../../common/types'; +import { CommonAlertParamDetails } from '../../../common/types/alerts'; import { AlertParamDuration } from '../flyout_expressions/alert_param_duration'; import { AlertParamType } from '../../../common/enums'; import { AlertParamPercentage } from '../flyout_expressions/alert_param_percentage'; @@ -26,7 +26,7 @@ export const Expression: React.FC = (props) => { const details = paramDetails[alertParamName]; const value = alertParams[alertParamName]; - switch (details.type) { + switch (details?.type) { case AlertParamType.Duration: return ( ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', diff --git a/x-pack/plugins/monitoring/public/alerts/panel.tsx b/x-pack/plugins/monitoring/public/alerts/panel.tsx index eb3b6ff9da1be..99db6c8b3c945 100644 --- a/x-pack/plugins/monitoring/public/alerts/panel.tsx +++ b/x-pack/plugins/monitoring/public/alerts/panel.tsx @@ -18,8 +18,7 @@ import { EuiListGroupItem, } from '@elastic/eui'; -import { CommonAlertStatus, CommonAlertState } from '../../common/types'; -import { AlertMessage } from '../../server/alerts/types'; +import { CommonAlertStatus, CommonAlertState, AlertMessage } from '../../common/types/alerts'; import { Legacy } from '../legacy_shims'; import { replaceTokens } from './lib/replace_tokens'; import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; diff --git a/x-pack/plugins/monitoring/public/alerts/status.tsx b/x-pack/plugins/monitoring/public/alerts/status.tsx index c1ad41fc8d763..53918807a4272 100644 --- a/x-pack/plugins/monitoring/public/alerts/status.tsx +++ b/x-pack/plugins/monitoring/public/alerts/status.tsx @@ -7,9 +7,8 @@ import React from 'react'; import { EuiToolTip, EuiHealth } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { CommonAlertStatus } from '../../common/types'; +import { CommonAlertStatus, AlertMessage, AlertState } from '../../common/types/alerts'; import { AlertSeverity } from '../../common/enums'; -import { AlertMessage, AlertState } from '../../server/alerts/types'; import { AlertsBadge } from './badge'; import { isInSetupMode } from '../lib/setup_mode'; import { SetupModeContext } from '../components/setup_mode/setup_mode_context'; diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx new file mode 100644 index 0000000000000..5e8e676448218 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer } from '@elastic/eui'; +import { Expression, Props } from '../components/duration/expression'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { CommonAlertParamDetails } from '../../../common/types/alerts'; + +interface ThreadPoolTypes { + [key: string]: unknown; +} + +interface ThreadPoolRejectionAlertDetails { + label: string; + paramDetails: CommonAlertParamDetails; +} + +export function createThreadPoolRejectionsAlertType( + alertType: string, + threadPoolAlertDetails: ThreadPoolRejectionAlertDetails +): AlertTypeModel { + return { + id: alertType, + name: threadPoolAlertDetails.label, + iconClass: 'bell', + alertParamsExpression: (props: Props) => ( + <> + + + + ), + validate: (inputValues: ThreadPoolTypes) => { + const errors: { [key: string]: string[] } = {}; + const value = inputValues.threshold as number; + if (value < 0) { + const errStr = i18n.translate('xpack.monitoring.alerts.validation.lessThanZero', { + defaultMessage: 'This value can not be less than zero', + }); + errors.threshold = [errStr]; + } + + if (!inputValues.duration) { + const errStr = i18n.translate('xpack.monitoring.alerts.validation.duration', { + defaultMessage: 'A valid duration is required.', + }); + errors.duration = [errStr]; + } + + return { errors }; + }, + defaultActionMessage: '{{context.internalFullMessage}}', + requiresAppContext: true, + }; +} diff --git a/x-pack/plugins/monitoring/public/angular/providers/private.js b/x-pack/plugins/monitoring/public/angular/providers/private.js index 3a667037b2919..7709865432fe6 100644 --- a/x-pack/plugins/monitoring/public/angular/providers/private.js +++ b/x-pack/plugins/monitoring/public/angular/providers/private.js @@ -81,9 +81,9 @@ * * @param {[type]} prov [description] */ -import _ from 'lodash'; +import { partial, uniqueId, isObject } from 'lodash'; -const nextId = _.partial(_.uniqueId, 'privateProvider#'); +const nextId = partial(uniqueId, 'privateProvider#'); function name(fn) { return fn.name || fn.toString().split('\n').shift(); @@ -141,7 +141,7 @@ export function PrivateProvider() { const context = {}; let instance = $injector.invoke(prov, context, locals); - if (!_.isObject(instance)) instance = context; + if (!isObject(instance)) instance = context; privPath.pop(); return instance; @@ -155,7 +155,7 @@ export function PrivateProvider() { if ($delegateId != null && $delegateProv != null) { instance = instantiate(prov, { - $decorate: _.partial(get, $delegateId, $delegateProv), + $decorate: partial(get, $delegateId, $delegateProv), }); } else { instance = instantiate(prov); diff --git a/x-pack/plugins/monitoring/public/components/chart/chart_target.js b/x-pack/plugins/monitoring/public/components/chart/chart_target.js index 9a590d803bb19..519964e4d5914 100644 --- a/x-pack/plugins/monitoring/public/components/chart/chart_target.js +++ b/x-pack/plugins/monitoring/public/components/chart/chart_target.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { get, isEqual, filter } from 'lodash'; import $ from 'jquery'; import React from 'react'; import { eventBus } from './event_bus'; @@ -50,12 +50,12 @@ export class ChartTarget extends React.Component { } UNSAFE_componentWillReceiveProps(newProps) { - if (this.plot && !_.isEqual(newProps, this.props)) { + if (this.plot && !isEqual(newProps, this.props)) { const { series, timeRange } = newProps; const xaxisOptions = this.plot.getAxes().xaxis.options; - xaxisOptions.min = _.get(timeRange, 'min'); - xaxisOptions.max = _.get(timeRange, 'max'); + xaxisOptions.min = get(timeRange, 'min'); + xaxisOptions.max = get(timeRange, 'max'); this.plot.setData(this.filterData(series, newProps.seriesToShow)); this.plot.setupGrid(); @@ -73,7 +73,7 @@ export class ChartTarget extends React.Component { } filterData(data, seriesToShow) { - return _(data).filter(this.filterByShow(seriesToShow)).value(); + return filter(data, this.filterByShow(seriesToShow)); } async getOptions() { @@ -128,7 +128,7 @@ export class ChartTarget extends React.Component { this.handleThorPlotHover = (_event, pos, item, originalPlot) => { if (this.plot !== originalPlot) { // the crosshair is set for the original chart already - this.plot.setCrosshair({ x: _.get(pos, 'x') }); + this.plot.setCrosshair({ x: get(pos, 'x') }); } this.props.updateLegend(pos, item); }; diff --git a/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js b/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js index eb32ee108e7b3..829994791f769 100644 --- a/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js +++ b/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { debounce, keys, has, includes, isFunction, difference, assign } from 'lodash'; import React from 'react'; import { getLastValue } from './get_last_value'; import { TimeseriesContainer } from './timeseries_container'; @@ -17,7 +17,7 @@ export class TimeseriesVisualization extends React.Component { constructor(props) { super(props); - this.debouncedUpdateLegend = _.debounce(this.updateLegend, DEBOUNCE_SLOW_MS); + this.debouncedUpdateLegend = debounce(this.updateLegend, DEBOUNCE_SLOW_MS); this.debouncedUpdateLegend = this.debouncedUpdateLegend.bind(this); this.toggleFilter = this.toggleFilter.bind(this); @@ -26,18 +26,18 @@ export class TimeseriesVisualization extends React.Component { this.state = { values: {}, - seriesToShow: _.keys(values), + seriesToShow: keys(values), ignoreVisibilityUpdates: false, }; } filterLegend(id) { - if (!_.has(this.state.values, id)) { + if (!has(this.state.values, id)) { return []; } - const notAllShown = _.keys(this.state.values).length !== this.state.seriesToShow.length; - const isCurrentlyShown = _.includes(this.state.seriesToShow, id); + const notAllShown = keys(this.state.values).length !== this.state.seriesToShow.length; + const isCurrentlyShown = includes(this.state.seriesToShow, id); const seriesToShow = []; if (notAllShown && isCurrentlyShown) { @@ -59,7 +59,7 @@ export class TimeseriesVisualization extends React.Component { toggleFilter(_event, id) { const seriesToShow = this.filterLegend(id); - if (_.isFunction(this.props.onFilter)) { + if (isFunction(this.props.onFilter)) { this.props.onFilter(seriesToShow); } } @@ -94,7 +94,7 @@ export class TimeseriesVisualization extends React.Component { getValuesByX(this.props.series, pos.x, setValueCallback); } } else { - _.assign(values, this.getLastValues()); + assign(values, this.getLastValues()); } this.setState({ values }); @@ -102,13 +102,13 @@ export class TimeseriesVisualization extends React.Component { UNSAFE_componentWillReceiveProps(props) { const values = this.getLastValues(props); - const currentKeys = _.keys(this.state.values); - const keys = _.keys(values); - const diff = _.difference(keys, currentKeys); + const currentKeys = keys(this.state.values); + const valueKeys = keys(values); + const diff = difference(valueKeys, currentKeys); const nextState = { values: values }; if (diff.length && !this.state.ignoreVisibilityUpdates) { - nextState.seriesToShow = keys; + nextState.seriesToShow = valueKeys; } this.setState(nextState); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 0fe434afa2c88..7e85d62c4bbd6 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -41,6 +41,8 @@ import { ALERT_CLUSTER_HEALTH, ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_NODES_CHANGED, ALERT_ELASTICSEARCH_VERSION_MISMATCH, @@ -162,6 +164,8 @@ const OVERVIEW_PANEL_ALERTS = [ALERT_CLUSTER_HEALTH, ALERT_LICENSE_EXPIRATION]; const NODES_PANEL_ALERTS = [ ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_NODES_CHANGED, ALERT_ELASTICSEARCH_VERSION_MISMATCH, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 41d3a579db5a2..61188487e2f99 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -27,7 +27,7 @@ import { EuiHealth, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; +import { get } from 'lodash'; import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { ListingCallOut } from '../../setup_mode/listing_callout'; @@ -58,7 +58,7 @@ const getNodeTooltip = (node) => { return null; }; -const getSortHandler = (type) => (item) => _.get(item, [type, 'summary', 'lastVal']); +const getSortHandler = (type) => (item) => get(item, [type, 'summary', 'lastVal']); const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, alerts) => { const cols = []; @@ -87,7 +87,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler let setupModeStatus = null; if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { - const list = _.get(setupMode, 'data.byUuid', {}); + const list = get(setupMode, 'data.byUuid', {}); const status = list[node.resolver] || {}; const instance = { uuid: node.resolver, @@ -396,7 +396,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear setupMode.data.totalUniqueInstanceCount ) { const finishMigrationAction = - _.get(setupMode.meta, 'liveClusterUuid') === clusterUuid + get(setupMode.meta, 'liveClusterUuid') === clusterUuid ? setupMode.shortcutToFinishMigration : setupMode.openFlyout; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js index 2c66d14a40605..5c8dca54894b4 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { sortBy } from 'lodash'; import React from 'react'; import { Shard } from './shard'; import { i18n } from '@kbn/i18n'; @@ -36,7 +36,7 @@ export class Unassigned extends React.Component { }; render() { - const shards = _.sortBy(this.props.shards, 'shard').map(this.createShard); + const shards = sortBy(this.props.shards, 'shard').map(this.createShard); return ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js index 47739b8fe31e8..a371f3e5ff40c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { some } from 'lodash'; export function hasPrimaryChildren(item) { - return _.some(item.children, { primary: true }); + return some(item.children, { primary: true }); } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js index db9b7bacc3cdf..335c3d29a5b9e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { each, isArray } from 'lodash'; export const _vents = {}; export const vents = { vents: _vents, on: function (id, cb) { - if (!_.isArray(_vents[id])) { + if (!isArray(_vents[id])) { _vents[id] = []; } _vents[id].push(cb); @@ -22,7 +22,7 @@ export const vents = { const args = Array.prototype.slice.call(arguments); const id = args.shift(); if (_vents[id]) { - _.each(_vents[id], function (cb) { + each(_vents[id], function (cb) { cb.apply(null, args); }); } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js index a9808ebc4c6ad..a04e2bcd1786e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { find, reduce, values, sortBy } from 'lodash'; import { decorateShards } from '../lib/decorate_shards'; export function indicesByNodes() { @@ -39,7 +39,7 @@ export function indicesByNodes() { return obj; } - let nodeObj = _.find(obj[index].children, { id: node }); + let nodeObj = find(obj[index].children, { id: node }); if (!nodeObj) { nodeObj = { id: node, @@ -55,7 +55,7 @@ export function indicesByNodes() { return obj; } - const data = _.reduce( + const data = reduce( decorateShards(shards, nodes), function (obj, shard) { obj = createIndex(obj, shard); @@ -64,10 +64,11 @@ export function indicesByNodes() { }, {} ); - - return _(data) - .values() - .sortBy((index) => [!index.unassignedPrimaries, /^\./.test(index.name), index.name]) - .value(); + const dataValues = values(data); + return sortBy(dataValues, (index) => [ + !index.unassignedPrimaries, + /^\./.test(index.name), + index.name, + ]); }; } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js index 353e1c23d4bc1..f8dd5b6cb8e8d 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { find, some, reduce, values, sortBy } from 'lodash'; import { hasPrimaryChildren } from '../lib/has_primary_children'; import { decorateShards } from '../lib/decorate_shards'; @@ -32,7 +32,7 @@ export function nodesByIndices() { if (!obj[node]) { createNode(obj, nodes[node], node); } - let indexObj = _.find(obj[node].children, { id: index }); + let indexObj = find(obj[node].children, { id: index }); if (!indexObj) { indexObj = { id: index, @@ -51,7 +51,7 @@ export function nodesByIndices() { } let data = {}; - if (_.some(shards, isUnassigned)) { + if (some(shards, isUnassigned)) { data.unassigned = { name: 'Unassigned', master: false, @@ -60,19 +60,15 @@ export function nodesByIndices() { }; } - data = _.reduce(decorateShards(shards, nodes), createIndexAddShard, data); - - return _(data) - .values() - .sortBy(function (node) { - return [node.name !== 'Unassigned', !node.master, node.name]; - }) - .map(function (node) { - if (node.name === 'Unassigned') { - node.unassignedPrimaries = node.children.some(hasPrimaryChildren); - } - return node; - }) - .value(); + data = reduce(decorateShards(shards, nodes), createIndexAddShard, data); + const dataValues = values(data); + return sortBy(dataValues, function (node) { + return [node.name !== 'Unassigned', !node.master, node.name]; + }).map(function (node) { + if (node.name === 'Unassigned') { + node.unassignedPrimaries = node.children.some(hasPrimaryChildren); + } + return node; + }); }; } diff --git a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap index deed4687e74f6..046f2bfa92247 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap @@ -248,6 +248,7 @@ exports[`ExplainCollectionEnabled should explain about xpack.monitoring.collecti type="button" > - + - + { - const existingCurrent = _.find(clusters, { cluster_uuid: globalState.cluster_uuid }); + const existingCurrent = find(clusters, { cluster_uuid: globalState.cluster_uuid }); if (existingCurrent) { return existingCurrent; } - const firstCluster = _.first(clusters); + const firstCluster = first(clusters); if (firstCluster && firstCluster.cluster_uuid) { return firstCluster; } diff --git a/x-pack/plugins/monitoring/public/lib/route_init.js b/x-pack/plugins/monitoring/public/lib/route_init.js index eebdfa8692f1a..97ff621ee3164 100644 --- a/x-pack/plugins/monitoring/public/lib/route_init.js +++ b/x-pack/plugins/monitoring/public/lib/route_init.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { isInSetupMode } from './setup_mode'; import { getClusterFromClusters } from './get_cluster_from_clusters'; @@ -13,7 +12,7 @@ export function routeInitProvider(Private, monitoringClusters, globalState, lice const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); function isOnPage(hash) { - return _.includes(window.location.hash, hash); + return window.location.hash.includes(hash); } /* diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 4c50abb40dd3d..a228c540761b8 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -22,11 +22,11 @@ import { UI_SETTINGS } from '../../../../src/plugins/data/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { MonitoringStartPluginDependencies, MonitoringConfig } from './types'; import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; -import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; -import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert'; -import { createLegacyAlertTypes } from './alerts/legacy_alert'; -import { createDiskUsageAlertType } from './alerts/disk_usage_alert'; -import { createMemoryUsageAlertType } from './alerts/memory_usage_alert'; +import { + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, + ALERT_DETAILS, +} from '../common/constants'; interface MonitoringSetupPluginDependencies { home?: HomePublicPluginSetup; @@ -40,7 +40,7 @@ export class MonitoringPlugin Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public setup( + public async setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { @@ -73,16 +73,7 @@ export class MonitoringPlugin }); } - const { alertTypeRegistry } = plugins.triggersActionsUi; - alertTypeRegistry.register(createCpuUsageAlertType()); - alertTypeRegistry.register(createDiskUsageAlertType()); - alertTypeRegistry.register(createMemoryUsageAlertType()); - alertTypeRegistry.register(createMissingMonitoringDataAlertType()); - - const legacyAlertTypes = createLegacyAlertTypes(); - for (const legacyAlertType of legacyAlertTypes) { - alertTypeRegistry.register(legacyAlertType); - } + await this.registerAlertsAsync(plugins); const app: App = { id, @@ -106,7 +97,6 @@ export class MonitoringPlugin usageCollection: plugins.usageCollection, }; - pluginsStart.kibanaLegacy.loadFontAwesome(); this.setInitialTimefilter(deps); const monitoringApp = new AngularApp(deps); @@ -154,4 +144,41 @@ export class MonitoringPlugin ['showCgroupMetricsLogstash', monitoring.ui.container.logstash.enabled], ]; } + + private registerAlertsAsync = async (plugins: MonitoringSetupPluginDependencies) => { + const { createCpuUsageAlertType } = await import('./alerts/cpu_usage_alert'); + const { createMissingMonitoringDataAlertType } = await import( + './alerts/missing_monitoring_data_alert' + ); + const { createLegacyAlertTypes } = await import('./alerts/legacy_alert'); + const { createDiskUsageAlertType } = await import('./alerts/disk_usage_alert'); + const { createThreadPoolRejectionsAlertType } = await import( + './alerts/thread_pool_rejections_alert' + ); + const { createMemoryUsageAlertType } = await import('./alerts/memory_usage_alert'); + + const { + triggersActionsUi: { alertTypeRegistry }, + } = plugins; + alertTypeRegistry.register(createCpuUsageAlertType()); + alertTypeRegistry.register(createDiskUsageAlertType()); + alertTypeRegistry.register(createMemoryUsageAlertType()); + alertTypeRegistry.register(createMissingMonitoringDataAlertType()); + alertTypeRegistry.register( + createThreadPoolRejectionsAlertType( + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS] + ) + ); + alertTypeRegistry.register( + createThreadPoolRejectionsAlertType( + ALERT_THREAD_POOL_WRITE_REJECTIONS, + ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS] + ) + ); + const legacyAlertTypes = createLegacyAlertTypes(); + for (const legacyAlertType of legacyAlertTypes) { + alertTypeRegistry.register(legacyAlertType); + } + }; } diff --git a/x-pack/plugins/monitoring/public/services/features.js b/x-pack/plugins/monitoring/public/services/features.js index f98af10f8dfb4..5e29353e497d1 100644 --- a/x-pack/plugins/monitoring/public/services/features.js +++ b/x-pack/plugins/monitoring/public/services/features.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { has, isUndefined } from 'lodash'; export function featuresProvider($window) { function getData() { @@ -28,11 +28,11 @@ export function featuresProvider($window) { function isEnabled(featureName, defaultSetting) { const monitoringDataObj = getData(); - if (_.has(monitoringDataObj, featureName)) { + if (has(monitoringDataObj, featureName)) { return monitoringDataObj[featureName]; } - if (_.isUndefined(defaultSetting)) { + if (isUndefined(defaultSetting)) { return false; } diff --git a/x-pack/plugins/monitoring/public/services/title.js b/x-pack/plugins/monitoring/public/services/title.js index 0715f4dc9e0b6..91ef4c32f3b98 100644 --- a/x-pack/plugins/monitoring/public/services/title.js +++ b/x-pack/plugins/monitoring/public/services/title.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Legacy } from '../legacy_shims'; export function titleProvider($rootScope) { return function changeTitle(cluster, suffix) { - let clusterName = _.get(cluster, 'cluster_name'); + let clusterName = get(cluster, 'cluster_name'); clusterName = clusterName ? `- ${clusterName}` : ''; suffix = suffix ? `- ${suffix}` : ''; $rootScope.$applyAsync(() => { diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js index 8021ae7e5f63c..7e78170d1117f 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js @@ -20,6 +20,8 @@ import { MonitoringViewBaseController } from '../../../base_controller'; import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, ALERT_MEMORY_USAGE, @@ -76,6 +78,8 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', { alertTypeIds: [ ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_MISSING_MONITORING_DATA, ], diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js index 5164e93c266ca..586261eecb250 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -21,6 +21,8 @@ import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, ALERT_MEMORY_USAGE, @@ -60,6 +62,8 @@ uiRoutes.when('/elasticsearch/nodes/:node', { alertTypeIds: [ ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_MISSING_MONITORING_DATA, ], diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js index e69d572f9560b..3ec9c6235867b 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -19,6 +19,8 @@ import { ELASTICSEARCH_SYSTEM_ID, CODE_PATH_ELASTICSEARCH, ALERT_CPU_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, ALERT_MEMORY_USAGE, @@ -93,6 +95,8 @@ uiRoutes.when('/elasticsearch/nodes', { alertTypeIds: [ ALERT_CPU_USAGE, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_MISSING_MONITORING_DATA, ], diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_common.ts b/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts similarity index 97% rename from x-pack/plugins/monitoring/server/alerts/alerts_common.ts rename to x-pack/plugins/monitoring/server/alerts/alert_helpers.ts index 41c8bba17df0a..984746e59f06b 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_common.ts +++ b/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { AlertMessageDocLinkToken } from './types'; +import { AlertMessageDocLinkToken } from '../../common/types/alerts'; import { AlertMessageTokenType } from '../../common/enums'; export class AlertingDefaults { diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts index f486061109b39..cc0423051f2aa 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts @@ -60,9 +60,4 @@ describe('AlertsFactory', () => { expect(alert).not.toBeNull(); expect(alert?.type).toBe(ALERT_CPU_USAGE); }); - - it('should get all', () => { - const alerts = AlertsFactory.getAll(); - expect(alerts.length).toBe(10); - }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts index 22c41c9c60038..efd3d7d5e3b30 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts @@ -8,6 +8,8 @@ import { CpuUsageAlert, MissingMonitoringDataAlert, DiskUsageAlert, + ThreadPoolSearchRejectionsAlert, + ThreadPoolWriteRejectionsAlert, MemoryUsageAlert, NodesChangedAlert, ClusterHealthAlert, @@ -23,6 +25,8 @@ import { ALERT_CPU_USAGE, ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, + ALERT_THREAD_POOL_SEARCH_REJECTIONS, + ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_MEMORY_USAGE, ALERT_NODES_CHANGED, ALERT_LOGSTASH_VERSION_MISMATCH, @@ -31,12 +35,14 @@ import { } from '../../common/constants'; import { AlertsClient } from '../../../alerts/server'; -export const BY_TYPE = { +const BY_TYPE = { [ALERT_CLUSTER_HEALTH]: ClusterHealthAlert, [ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert, [ALERT_CPU_USAGE]: CpuUsageAlert, [ALERT_MISSING_MONITORING_DATA]: MissingMonitoringDataAlert, [ALERT_DISK_USAGE]: DiskUsageAlert, + [ALERT_THREAD_POOL_SEARCH_REJECTIONS]: ThreadPoolSearchRejectionsAlert, + [ALERT_THREAD_POOL_WRITE_REJECTIONS]: ThreadPoolWriteRejectionsAlert, [ALERT_MEMORY_USAGE]: MemoryUsageAlert, [ALERT_NODES_CHANGED]: NodesChangedAlert, [ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert, diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index c92291cf72093..48b783a450807 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -28,14 +28,16 @@ import { AlertData, AlertInstanceState, AlertEnableAction, -} from './types'; + CommonAlertFilter, + CommonAlertParams, + CommonBaseAlert, +} from '../../common/types/alerts'; import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { MonitoringConfig } from '../config'; import { AlertSeverity } from '../../common/enums'; -import { CommonAlertFilter, CommonAlertParams, CommonBaseAlert } from '../../common/types'; import { MonitoringLicenseService } from '../types'; import { mbSafeQuery } from '../lib/mb_safe_query'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; @@ -269,18 +271,18 @@ export class BaseAlert { } protected async fetchData( - params: CommonAlertParams, + params: CommonAlertParams | unknown, callCluster: any, clusters: AlertCluster[], uiSettings: IUiSettingsClient, availableCcs: string[] - ): Promise { + ): Promise> { // Child should implement throw new Error('Child classes must implement `fetchData`'); } protected async processData( - data: AlertData[], + data: Array, clusters: AlertCluster[], services: AlertServices, logger: Logger, @@ -365,15 +367,18 @@ export class BaseAlert { }; } - protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + protected getUiMessage( + alertState: AlertState | unknown, + item: AlertData | unknown + ): AlertMessage { throw new Error('Child classes must implement `getUiMessage`'); } protected executeActions( instance: AlertInstance, - instanceState: AlertInstanceState, - item: AlertData, - cluster: AlertCluster + instanceState: AlertInstanceState | unknown, + item: AlertData | unknown, + cluster?: AlertCluster | unknown ) { throw new Error('Child classes must implement `executeActions`'); } diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index 427dd2f86de00..1d3d36413ebc2 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -14,15 +14,15 @@ import { AlertMessageLinkToken, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_CLUSTER_HEALTH } from '../../common/constants'; +import { INDEX_ALERTS, ALERT_CLUSTER_HEALTH, LEGACY_ALERT_DETAILS } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertClusterHealthType } from '../../common/enums'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; -import { CommonAlertParams } from '../../common/types'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', { defaultMessage: 'Allocate missing primary and replica shards', @@ -39,9 +39,7 @@ const WATCH_NAME = 'elasticsearch_cluster_status'; export class ClusterHealthAlert extends BaseAlert { public type = ALERT_CLUSTER_HEALTH; - public label = i18n.translate('xpack.monitoring.alerts.clusterHealth.label', { - defaultMessage: 'Cluster health', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index 09133dadca162..55931e2996cbf 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -16,55 +16,36 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertInstanceState, -} from './types'; + CommonAlertFilter, + CommonAlertNodeUuidFilter, + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance, AlertServices } from '../../../alerts/server'; -import { INDEX_PATTERN_ELASTICSEARCH, ALERT_CPU_USAGE } from '../../common/constants'; +import { + INDEX_PATTERN_ELASTICSEARCH, + ALERT_CPU_USAGE, + ALERT_DETAILS, +} from '../../common/constants'; import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance } from '../../../alerts/common'; import { parseDuration } from '../../../alerts/common/parse_duration'; -import { - CommonAlertFilter, - CommonAlertNodeUuidFilter, - CommonAlertParams, - CommonAlertParamDetail, -} from '../../common/types'; -import { AlertingDefaults, createLink } from './alerts_common'; +import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; -const DEFAULT_THRESHOLD = 85; -const DEFAULT_DURATION = '5m'; - interface CpuUsageParams { threshold: number; duration: string; } export class CpuUsageAlert extends BaseAlert { - public static paramDetails = { - threshold: { - label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.threshold.label', { - defaultMessage: `Notify when CPU is over`, - }), - type: AlertParamType.Percentage, - } as CommonAlertParamDetail, - duration: { - label: i18n.translate('xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label', { - defaultMessage: `Look at the average over`, - }), - type: AlertParamType.Duration, - } as CommonAlertParamDetail, - }; - public type = ALERT_CPU_USAGE; - public label = i18n.translate('xpack.monitoring.alerts.cpuUsage.label', { - defaultMessage: 'CPU Usage', - }); + public label = ALERT_DETAILS[ALERT_CPU_USAGE].label; protected defaultParams: CpuUsageParams = { - threshold: DEFAULT_THRESHOLD, - duration: DEFAULT_DURATION, + threshold: 85, + duration: '5m', }; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts index 34c640de79625..e54e736724357 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts @@ -15,43 +15,25 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertInstanceState, -} from './types'; + CommonAlertFilter, + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance, AlertServices } from '../../../alerts/server'; -import { INDEX_PATTERN_ELASTICSEARCH, ALERT_DISK_USAGE } from '../../common/constants'; +import { + INDEX_PATTERN_ELASTICSEARCH, + ALERT_DISK_USAGE, + ALERT_DETAILS, +} from '../../common/constants'; import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance } from '../../../alerts/common'; -import { CommonAlertFilter, CommonAlertParams, CommonAlertParamDetail } from '../../common/types'; -import { AlertingDefaults, createLink } from './alerts_common'; +import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; -interface ParamDetails { - [key: string]: CommonAlertParamDetail; -} - export class DiskUsageAlert extends BaseAlert { - public static readonly PARAM_DETAILS: ParamDetails = { - threshold: { - label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label', { - defaultMessage: `Notify when disk capacity is over`, - }), - type: AlertParamType.Percentage, - }, - duration: { - label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.duration.label', { - defaultMessage: `Look at the average over`, - }), - type: AlertParamType.Duration, - }, - }; - public static paramDetails = DiskUsageAlert.PARAM_DETAILS; - public static readonly TYPE = ALERT_DISK_USAGE; - public static readonly LABEL = i18n.translate('xpack.monitoring.alerts.diskUsage.label', { - defaultMessage: 'Disk Usage', - }); - public type = DiskUsageAlert.TYPE; - public label = DiskUsageAlert.LABEL; + public type = ALERT_DISK_USAGE; + public label = ALERT_DETAILS[ALERT_DISK_USAGE].label; protected defaultParams = { threshold: 80, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index f26b21f0c64c5..6412dcfde54bd 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -13,22 +13,24 @@ import { AlertMessage, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; +import { + INDEX_ALERTS, + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, +} from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertSeverity } from '../../common/enums'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'elasticsearch_version_mismatch'; export class ElasticsearchVersionMismatchAlert extends BaseAlert { public type = ALERT_ELASTICSEARCH_VERSION_MISMATCH; - public label = i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.label', { - defaultMessage: 'Elasticsearch version mismatch', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts index 48254f2dec326..5fa718dfb34cd 100644 --- a/x-pack/plugins/monitoring/server/alerts/index.ts +++ b/x-pack/plugins/monitoring/server/alerts/index.ts @@ -8,6 +8,8 @@ export { BaseAlert } from './base_alert'; export { CpuUsageAlert } from './cpu_usage_alert'; export { MissingMonitoringDataAlert } from './missing_monitoring_data_alert'; export { DiskUsageAlert } from './disk_usage_alert'; +export { ThreadPoolSearchRejectionsAlert } from './thread_pool_search_rejections_alert'; +export { ThreadPoolWriteRejectionsAlert } from './thread_pool_write_rejections_alert'; export { MemoryUsageAlert } from './memory_usage_alert'; export { ClusterHealthAlert } from './cluster_health_alert'; export { LicenseExpirationAlert } from './license_expiration_alert'; @@ -15,4 +17,4 @@ export { NodesChangedAlert } from './nodes_changed_alert'; export { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; export { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; export { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; -export { AlertsFactory, BY_TYPE } from './alerts_factory'; +export { AlertsFactory } from './alerts_factory'; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index 316f305603964..851a401635792 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -13,22 +13,24 @@ import { AlertMessage, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; +import { + INDEX_ALERTS, + ALERT_KIBANA_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, +} from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertSeverity } from '../../common/enums'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'kibana_version_mismatch'; export class KibanaVersionMismatchAlert extends BaseAlert { public type = ALERT_KIBANA_VERSION_MISMATCH; - public label = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.label', { - defaultMessage: 'Kibana version mismatch', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index f1412ff0fc91a..e0396ee6673e8 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -16,27 +16,26 @@ import { AlertMessageLinkToken, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; import { INDEX_ALERTS, ALERT_LICENSE_EXPIRATION, FORMAT_DURATION_TEMPLATE_SHORT, + LEGACY_ALERT_DETAILS, } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType } from '../../common/enums'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'xpack_license_expiration'; export class LicenseExpirationAlert extends BaseAlert { public type = ALERT_LICENSE_EXPIRATION; - public label = i18n.translate('xpack.monitoring.alerts.licenseExpiration.label', { - defaultMessage: 'License expiration', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label; public isLegacy = true; protected actionVariables = [ { diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 37515e32e591a..7f5c0ea40e36a 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -13,22 +13,24 @@ import { AlertMessage, AlertInstanceState, LegacyAlert, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; +import { + INDEX_ALERTS, + ALERT_LOGSTASH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, +} from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertSeverity } from '../../common/enums'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'logstash_version_mismatch'; export class LogstashVersionMismatchAlert extends BaseAlert { public type = ALERT_LOGSTASH_VERSION_MISMATCH; - public label = i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.label', { - defaultMessage: 'Logstash version mismatch', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts index 8dc707afab1e1..c37176764c020 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts @@ -15,44 +15,26 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertInstanceState, -} from './types'; + CommonAlertFilter, + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance, AlertServices } from '../../../alerts/server'; -import { INDEX_PATTERN_ELASTICSEARCH, ALERT_MEMORY_USAGE } from '../../common/constants'; +import { + INDEX_PATTERN_ELASTICSEARCH, + ALERT_MEMORY_USAGE, + ALERT_DETAILS, +} from '../../common/constants'; import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance } from '../../../alerts/common'; -import { CommonAlertFilter, CommonAlertParams, CommonAlertParamDetail } from '../../common/types'; -import { AlertingDefaults, createLink } from './alerts_common'; +import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerts/common/parse_duration'; -interface ParamDetails { - [key: string]: CommonAlertParamDetail; -} - export class MemoryUsageAlert extends BaseAlert { - public static readonly PARAM_DETAILS: ParamDetails = { - threshold: { - label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label', { - defaultMessage: `Notify when memory usage is over`, - }), - type: AlertParamType.Percentage, - }, - duration: { - label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label', { - defaultMessage: `Look at the average over`, - }), - type: AlertParamType.Duration, - }, - }; - public static paramDetails = MemoryUsageAlert.PARAM_DETAILS; - public static readonly TYPE = ALERT_MEMORY_USAGE; - public static readonly LABEL = i18n.translate('xpack.monitoring.alerts.memoryUsage.label', { - defaultMessage: 'Memory Usage (JVM)', - }); - public type = MemoryUsageAlert.TYPE; - public label = MemoryUsageAlert.LABEL; + public type = ALERT_MEMORY_USAGE; + public label = ALERT_DETAILS[ALERT_MEMORY_USAGE].label; protected defaultParams = { threshold: 85, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts index 5b4542a4439ca..456ad92855f65 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts @@ -16,24 +16,22 @@ import { AlertMissingData, AlertMessageTimeToken, AlertInstanceState, -} from './types'; + CommonAlertFilter, + CommonAlertParams, + CommonAlertStackProductFilter, + CommonAlertNodeUuidFilter, +} from '../../common/types/alerts'; import { AlertInstance, AlertServices } from '../../../alerts/server'; import { INDEX_PATTERN, ALERT_MISSING_MONITORING_DATA, INDEX_PATTERN_ELASTICSEARCH, + ALERT_DETAILS, } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance } from '../../../alerts/common'; import { parseDuration } from '../../../alerts/common/parse_duration'; -import { - CommonAlertFilter, - CommonAlertParams, - CommonAlertParamDetail, - CommonAlertStackProductFilter, - CommonAlertNodeUuidFilter, -} from '../../common/types'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; import { getTypeLabelForStackProduct } from '../lib/alerts/get_type_label_for_stack_product'; @@ -41,7 +39,7 @@ import { getListingLinkForStackProduct } from '../lib/alerts/get_listing_link_fo import { getStackProductLabel } from '../lib/alerts/get_stack_product_label'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; -import { AlertingDefaults, createLink } from './alerts_common'; +import { AlertingDefaults, createLink } from './alert_helpers'; const RESOLVED = i18n.translate('xpack.monitoring.alerts.missingData.resolved', { defaultMessage: 'resolved', @@ -62,27 +60,10 @@ interface MissingDataParams { } export class MissingMonitoringDataAlert extends BaseAlert { - public static paramDetails = { - duration: { - label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', { - defaultMessage: `Notify if monitoring data is missing for the last`, - }), - type: AlertParamType.Duration, - } as CommonAlertParamDetail, - limit: { - label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.limit.label', { - defaultMessage: `looking back`, - }), - type: AlertParamType.Duration, - } as CommonAlertParamDetail, - }; - public defaultThrottle: string = '6h'; public type = ALERT_MISSING_MONITORING_DATA; - public label = i18n.translate('xpack.monitoring.alerts.missingData.label', { - defaultMessage: 'Missing monitoring data', - }); + public label = ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].label; protected defaultParams: MissingDataParams = { duration: DEFAULT_DURATION, diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index e03e6ea53ab4e..7b54ef629cba6 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -14,22 +14,20 @@ import { AlertInstanceState, LegacyAlert, LegacyAlertNodesChangedList, -} from './types'; + CommonAlertParams, +} from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { INDEX_ALERTS, ALERT_NODES_CHANGED } from '../../common/constants'; +import { INDEX_ALERTS, ALERT_NODES_CHANGED, LEGACY_ALERT_DETAILS } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { CommonAlertParams } from '../../common/types'; import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; -import { AlertingDefaults } from './alerts_common'; +import { AlertingDefaults } from './alert_helpers'; const WATCH_NAME = 'elasticsearch_nodes'; export class NodesChangedAlert extends BaseAlert { public type = ALERT_NODES_CHANGED; - public label = i18n.translate('xpack.monitoring.alerts.nodesChanged.label', { - defaultMessage: 'Nodes changed', - }); + public label = LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label; public isLegacy = true; protected actionVariables = [ diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts new file mode 100644 index 0000000000000..4905ae73b0545 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts @@ -0,0 +1,312 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient, Logger } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertMessage, + AlertThreadPoolRejectionsState, + AlertMessageTimeToken, + AlertMessageLinkToken, + CommonAlertFilter, + ThreadPoolRejectionsAlertParams, +} from '../../common/types/alerts'; +import { AlertInstance, AlertServices } from '../../../alerts/server'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; +import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; +import { Alert, RawAlertInstance } from '../../../alerts/common'; +import { AlertingDefaults, createLink } from './alert_helpers'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; + +type ActionVariables = Array<{ name: string; description: string }>; + +export class ThreadPoolRejectionsAlertBase extends BaseAlert { + protected static createActionVariables(type: string) { + return [ + { + name: 'count', + description: i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.actionVariables.count', + { + defaultMessage: 'The number of nodes reporting high thread pool {type} rejections.', + values: { type }, + } + ), + }, + ...Object.values(AlertingDefaults.ALERT_TYPE.context), + ]; + } + + protected defaultParams: ThreadPoolRejectionsAlertParams = { + threshold: 300, + duration: '5m', + }; + + constructor( + rawAlert: Alert | undefined = undefined, + public readonly type: string, + public readonly threadPoolType: string, + public readonly label: string, + public readonly actionVariables: ActionVariables + ) { + super(rawAlert); + } + + protected async fetchData( + params: ThreadPoolRejectionsAlertParams, + callCluster: any, + clusters: AlertCluster[], + uiSettings: IUiSettingsClient, + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + + const { threshold, duration } = params; + + const stats = await fetchThreadPoolRejectionStats( + callCluster, + clusters, + esIndexPattern, + this.config.ui.max_bucket_size, + this.threadPoolType, + duration + ); + + return stats.map((stat) => { + const { clusterUuid, nodeId, rejectionCount, ccs } = stat; + + return { + instanceKey: `${clusterUuid}:${nodeId}`, + shouldFire: rejectionCount > threshold, + rejectionCount, + severity: AlertSeverity.Danger, + meta: stat, + clusterUuid, + ccs, + }; + }); + } + + protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) { + const alertInstanceStates = alertInstance.state + ?.alertStates as AlertThreadPoolRejectionsState[]; + const nodeUuid = filters?.find((filter) => filter.nodeUuid)?.nodeUuid; + + if (!alertInstanceStates?.length || !nodeUuid) { + return true; + } + + const nodeAlerts = alertInstanceStates.filter(({ nodeId }) => nodeId === nodeUuid); + return Boolean(nodeAlerts.length); + } + + protected getUiMessage( + alertState: AlertThreadPoolRejectionsState, + rejectionCount: number + ): AlertMessage { + const { nodeName, nodeId } = alertState; + return { + text: i18n.translate('xpack.monitoring.alerts.threadPoolRejections.ui.firingMessage', { + defaultMessage: `Node #start_link{nodeName}#end_link is reporting {rejectionCount} {type} rejections at #absolute`, + values: { + nodeName, + type: this.threadPoolType, + rejectionCount, + }, + }), + nextSteps: [ + createLink( + i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.monitorThisNode', + { + defaultMessage: `#start_linkMonitor this node#end_link`, + } + ), + `elasticsearch/nodes/${nodeId}/advanced`, + AlertMessageTokenType.Link + ), + createLink( + i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.optimizeQueries', + { + defaultMessage: '#start_linkOptimize complex queries#end_link', + } + ), + `{elasticWebsiteUrl}blog/advanced-tuning-finding-and-fixing-slow-elasticsearch-queries` + ), + createLink( + i18n.translate('xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.addMoreNodes', { + defaultMessage: '#start_linkAdd more nodes#end_link', + }), + `{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html` + ), + createLink( + i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.resizeYourDeployment', + { + defaultMessage: '#start_linkResize your deployment (ECE)#end_link', + } + ), + `{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html` + ), + createLink( + i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.ui.nextSteps.threadPoolSettings', + { + defaultMessage: '#start_linkThread pool settings#end_link', + } + ), + `{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/modules-threadpool.html` + ), + ], + tokens: [ + { + startToken: '#absolute', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: alertState.ui.triggeredMS, + } as AlertMessageTimeToken, + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.Link, + url: `elasticsearch/nodes/${nodeId}`, + } as AlertMessageLinkToken, + ], + }; + } + + protected executeActions( + instance: AlertInstance, + alertStates: AlertThreadPoolRejectionsState[], + cluster: AlertCluster + ) { + const type = this.threadPoolType; + const count = alertStates.length; + const { clusterName: clusterKnownName, clusterUuid } = cluster; + const clusterName = clusterKnownName || clusterUuid; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.shortAction', + { + defaultMessage: 'Verify thread pool {type} rejections across affected nodes.', + values: { + type, + }, + } + ); + + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.fullAction', + { + defaultMessage: 'View nodes', + } + ); + + const ccs = alertStates.find((state) => state.ccs)?.ccs; + const globalStateLink = this.createGlobalStateLink('elasticsearch/nodes', clusterUuid, ccs); + + const action = `[${fullActionText}](${globalStateLink})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.firing.internalShortMessage', + { + defaultMessage: `Thread pool {type} rejections alert is firing for {count} node(s) in cluster: {clusterName}. {shortActionText}`, + values: { + count, + clusterName, + shortActionText, + type, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.threadPoolRejections.firing.internalFullMessage', + { + defaultMessage: `Thread pool {type} rejections alert is firing for {count} node(s) in cluster: {clusterName}. {action}`, + values: { + count, + clusterName, + action, + type, + }, + } + ); + + instance.scheduleActions('default', { + internalShortMessage, + internalFullMessage: this.isCloud ? internalShortMessage : internalFullMessage, + threadPoolType: type, + state: AlertingDefaults.ALERT_STATE.firing, + count, + clusterName, + action, + actionPlain: shortActionText, + }); + } + + protected async processData( + data: AlertData[], + clusters: AlertCluster[], + services: AlertServices, + logger: Logger, + state: { lastChecked?: number } + ) { + const currentUTC = +new Date(); + for (const cluster of clusters) { + const nodes = data.filter((node) => node.clusterUuid === cluster.clusterUuid); + if (!nodes.length) { + continue; + } + + const firingNodeUuids = nodes.filter((node) => node.shouldFire); + + if (!firingNodeUuids.length) { + continue; + } + + const instanceSuffix = firingNodeUuids.map((node) => node.meta.nodeId); + + const instancePrefix = `${this.type}:${cluster.clusterUuid}:`; + const alertInstanceId = `${instancePrefix}:${instanceSuffix}`; + const alertInstance = services.alertInstanceFactory(alertInstanceId); + const newAlertStates: AlertThreadPoolRejectionsState[] = []; + + for (const node of nodes) { + if (!node.shouldFire) { + continue; + } + const stat = node.meta as AlertThreadPoolRejectionsState; + const nodeState = this.getDefaultAlertState( + cluster, + node + ) as AlertThreadPoolRejectionsState; + const { nodeId, nodeName, rejectionCount } = stat; + nodeState.nodeId = nodeId; + nodeState.nodeName = nodeName; + nodeState.ui.triggeredMS = currentUTC; + nodeState.ui.isFiring = true; + nodeState.ui.severity = node.severity; + nodeState.ui.message = this.getUiMessage(nodeState, rejectionCount); + newAlertStates.push(nodeState); + } + + alertInstance.replaceState({ alertStates: newAlertStates }); + if (newAlertStates.length) { + this.executeActions(alertInstance, newAlertStates, cluster); + } + } + + state.lastChecked = currentUTC; + return state; + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts new file mode 100644 index 0000000000000..10df95c05ba3f --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base'; +import { ALERT_THREAD_POOL_SEARCH_REJECTIONS, ALERT_DETAILS } from '../../common/constants'; +import { Alert } from '../../../alerts/common'; + +export class ThreadPoolSearchRejectionsAlert extends ThreadPoolRejectionsAlertBase { + private static TYPE = ALERT_THREAD_POOL_SEARCH_REJECTIONS; + private static THREAD_POOL_TYPE = 'search'; + private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS].label; + constructor(rawAlert?: Alert) { + super( + rawAlert, + ThreadPoolSearchRejectionsAlert.TYPE, + ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE, + ThreadPoolSearchRejectionsAlert.LABEL, + ThreadPoolRejectionsAlertBase.createActionVariables( + ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE + ) + ); + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts new file mode 100644 index 0000000000000..d415515315b37 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base'; +import { ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_DETAILS } from '../../common/constants'; +import { Alert } from '../../../alerts/common'; + +export class ThreadPoolWriteRejectionsAlert extends ThreadPoolRejectionsAlertBase { + private static TYPE = ALERT_THREAD_POOL_WRITE_REJECTIONS; + private static THREAD_POOL_TYPE = 'write'; + private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS].label; + constructor(rawAlert?: Alert) { + super( + rawAlert, + ThreadPoolWriteRejectionsAlert.TYPE, + ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE, + ThreadPoolWriteRejectionsAlert.LABEL, + ThreadPoolRejectionsAlertBase.createActionVariables( + ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE + ) + ); + } +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index d474338bce922..368a909279b8c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { AlertCluster } from '../../alerts/types'; +import { AlertCluster } from '../../../common/types/alerts'; interface RangeFilter { [field: string]: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index ecd324c083a8c..b38a32164223e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import moment from 'moment'; import { NORMALIZED_DERIVATIVE_UNIT } from '../../../common/constants'; -import { AlertCluster, AlertCpuUsageNodeStats } from '../../alerts/types'; +import { AlertCluster, AlertCpuUsageNodeStats } from '../../../common/types/alerts'; interface NodeBucketESResponse { key: string; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index 6201204ebebe0..f00c42d708b16 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { AlertCluster, AlertDiskUsageNodeStats } from '../../alerts/types'; +import { AlertCluster, AlertDiskUsageNodeStats } from '../../../common/types/alerts'; export async function fetchDiskUsageNodeStats( callCluster: any, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts index fe01a1b921c2e..fbf7608a737ba 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../alerts/types'; +import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../../common/types/alerts'; export async function fetchLegacyAlerts( callCluster: any, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index c6843c3ed5f12..9a68b3afc7758 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { AlertCluster, AlertMemoryUsageNodeStats } from '../../alerts/types'; +import { AlertCluster, AlertMemoryUsageNodeStats } from '../../../common/types/alerts'; export async function fetchMemoryUsageNodeStats( callCluster: any, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index 91fc05137a8c1..49307764e9f01 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { AlertCluster, AlertMissingData } from '../../alerts/types'; +import { AlertCluster, AlertMissingData } from '../../../common/types/alerts'; import { KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index 824eeab7245b4..c31ab91866b1d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -5,7 +5,7 @@ */ import { fetchStatus } from './fetch_status'; -import { AlertUiState, AlertState } from '../../alerts/types'; +import { AlertUiState, AlertState } from '../../../common/types/alerts'; import { AlertSeverity } from '../../../common/enums'; import { ALERT_CPU_USAGE, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index ed49f42e4908c..ed860ee21344d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import moment from 'moment'; -import { AlertInstanceState } from '../../alerts/types'; +import { AlertInstanceState } from '../../../common/types/alerts'; import { AlertsClient } from '../../../../alerts/server'; import { AlertsFactory } from '../../alerts'; -import { CommonAlertStatus, CommonAlertState, CommonAlertFilter } from '../../../common/types'; +import { + CommonAlertStatus, + CommonAlertState, + CommonAlertFilter, +} from '../../../common/types/alerts'; import { ALERTS } from '../../../common/constants'; import { MonitoringLicenseService } from '../../types'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts new file mode 100644 index 0000000000000..664ceb1d9411b --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { AlertCluster, AlertThreadPoolRejectionsStats } from '../../../common/types/alerts'; + +const invalidNumberValue = (value: number) => { + return isNaN(value) || value === undefined || value === null; +}; + +const getTopHits = (threadType: string, order: string) => ({ + top_hits: { + sort: [ + { + timestamp: { + order, + unmapped_type: 'long', + }, + }, + ], + _source: { + includes: [`node_stats.thread_pool.${threadType}.rejected`, 'source_node.name'], + }, + size: 1, + }, +}); + +export async function fetchThreadPoolRejectionStats( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number, + threadType: string, + duration: string +): Promise { + const clustersIds = clusters.map((cluster) => cluster.clusterUuid); + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clustersIds, + }, + }, + { + term: { + type: 'node_stats', + }, + }, + { + range: { + timestamp: { + gte: `now-${duration}`, + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size, + }, + aggs: { + nodes: { + terms: { + field: 'source_node.uuid', + size, + }, + aggs: { + most_recent: { + ...getTopHits(threadType, 'desc'), + }, + least_recent: { + ...getTopHits(threadType, 'asc'), + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const stats: AlertThreadPoolRejectionsStats[] = []; + const { buckets: clusterBuckets = [] } = response.aggregations.clusters; + + if (!clusterBuckets.length) { + return stats; + } + + for (const clusterBucket of clusterBuckets) { + for (const node of clusterBucket.nodes.buckets) { + const mostRecentDoc = get(node, 'most_recent.hits.hits[0]'); + mostRecentDoc.timestamp = mostRecentDoc.sort[0]; + + const leastRecentDoc = get(node, 'least_recent.hits.hits[0]'); + leastRecentDoc.timestamp = leastRecentDoc.sort[0]; + + if (!mostRecentDoc || mostRecentDoc.timestamp === leastRecentDoc.timestamp) { + continue; + } + + const rejectedPath = `_source.node_stats.thread_pool.${threadType}.rejected`; + const newRejectionCount = Number(get(mostRecentDoc, rejectedPath)); + const oldRejectionCount = Number(get(leastRecentDoc, rejectedPath)); + + if (invalidNumberValue(newRejectionCount) || invalidNumberValue(oldRejectionCount)) { + continue; + } + + const rejectionCount = + oldRejectionCount > newRejectionCount + ? newRejectionCount + : newRejectionCount - oldRejectionCount; + const indexName = mostRecentDoc._index; + const nodeName = get(mostRecentDoc, '_source.source_node.name') || node.key; + const nodeStat = { + rejectionCount, + type: threadType, + clusterUuid: clusterBucket.key, + nodeId: node.key, + nodeName, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + stats.push(nodeStat); + } + } + return stats; +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts index d97bc34c2adb0..29a27ac3d05e7 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { handleError } from '../../../../lib/errors'; import { RouteDependencies } from '../../../../types'; import { fetchStatus } from '../../../../lib/alerts/fetch_status'; -import { CommonAlertFilter } from '../../../../../common/types'; +import { CommonAlertFilter } from '../../../../../common/types/alerts'; export function alertStatusRoute(server: any, npRoute: RouteDependencies) { npRoute.router.post( diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx index e04e8f050006a..6a05749df6d7a 100644 --- a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx @@ -13,7 +13,7 @@ describe('EmptySection', () => { const section: ISection = { id: 'apm', title: 'APM', - icon: 'logoAPM', + icon: 'logoObservability', description: 'foo bar', }; const { getByText, queryAllByText } = render(); @@ -26,7 +26,7 @@ describe('EmptySection', () => { const section: ISection = { id: 'apm', title: 'APM', - icon: 'logoAPM', + icon: 'logoObservability', description: 'foo bar', linkTitle: 'install agent', href: 'https://www.elastic.co', diff --git a/x-pack/plugins/observability/public/pages/home/section.ts b/x-pack/plugins/observability/public/pages/home/section.ts index 8c87f17c16b3d..21bda1ac5bbba 100644 --- a/x-pack/plugins/observability/public/pages/home/section.ts +++ b/x-pack/plugins/observability/public/pages/home/section.ts @@ -24,7 +24,7 @@ export const appsSection: ISection[] = [ title: i18n.translate('xpack.observability.section.apps.apm.title', { defaultMessage: 'APM', }), - icon: 'logoAPM', + icon: 'logoObservability', description: i18n.translate('xpack.observability.section.apps.apm.description', { defaultMessage: 'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.', diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts index e13efbb8ffdd2..95b46cbb782d8 100644 --- a/x-pack/plugins/observability/public/pages/overview/empty_section.ts +++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts @@ -29,7 +29,7 @@ export const getEmptySections = ({ core }: { core: AppMountContext['core'] }): I title: i18n.translate('xpack.observability.emptySection.apps.apm.title', { defaultMessage: 'APM', }), - icon: 'logoAPM', + icon: 'logoObservability', description: i18n.translate('xpack.observability.emptySection.apps.apm.description', { defaultMessage: 'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.', @@ -74,7 +74,7 @@ export const getEmptySections = ({ core }: { core: AppMountContext['core'] }): I title: i18n.translate('xpack.observability.emptySection.apps.ux.title', { defaultMessage: 'User Experience', }), - icon: 'logoAPM', + icon: 'logoObservability', description: i18n.translate('xpack.observability.emptySection.apps.ux.description', { defaultMessage: 'Performance is a distribution. Measure the experiences of all visitors to your web application and understand how to improve the experience for everyone.', diff --git a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx index e6a61995ac78a..c6c0bee98a466 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx @@ -102,7 +102,6 @@ export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset, isOpen={isHelpOpen} closePopover={() => setIsHelpOpen(false)} panelPaddingSize="none" - withTitle anchorPosition="upLeft" > diff --git a/x-pack/plugins/painless_lab/public/styles/_index.scss b/x-pack/plugins/painless_lab/public/styles/_index.scss index 0a6b84523424a..14a58fe4bdb8b 100644 --- a/x-pack/plugins/painless_lab/public/styles/_index.scss +++ b/x-pack/plugins/painless_lab/public/styles/_index.scss @@ -1,5 +1,4 @@ @import '@elastic/eui/src/global_styling/variables/header'; -@import '@elastic/eui/src/components/nav_drawer/variables'; /** * This is a very brittle way of preventing the editor and other content from disappearing @@ -51,16 +50,3 @@ $headerOffset: $euiHeaderHeightCompensation * 3; // The panels container should adopt the height of the main container height: 100%; } - -/** - * 1. Hack EUI so the bottom bar doesn't obscure the nav drawer flyout, but is also not obscured - * by the main content area. - */ -.painlessLab__bottomBar { - z-index: 5; /* 1 */ - left: $euiNavDrawerWidthCollapsed; -} - -.painlessLab__bottomBar-isNavDrawerLocked { - left: $euiNavDrawerWidthExpanded; -} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index 4f54c2f9a1675..ff0caecf93a96 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -1270,6 +1270,7 @@ exports[`RemoteClusterForm proxy mode renders correct connection settings when u > diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap index 1124924fcdda2..f33e322cbaae4 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap @@ -71,14 +71,21 @@ exports[`OverwrittenSessionPage renders as expected 1`] = ` href="/mock-base-path/" > { isOpen={isMenuOpen} closePopover={() => setIsMenuOpen(false)} panelPaddingSize="none" - withTitle anchorPosition="downLeft" > diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx index a07c2e1c14ac4..57828c50ca17f 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx @@ -249,7 +249,7 @@ export class FeatureTable extends Component { ); const extraAction = ( - + {feature.reserved.description} ); @@ -320,6 +320,15 @@ export class FeatureTable extends Component { options={options} idSelected={`${feature.id}_${selectedPrivilegeId ?? NO_PRIVILEGE_VALUE}`} onChange={this.onChange(feature.id)} + legend={i18n.translate('xpack.security.management.editRole.featureTable.actionLegendText', { + defaultMessage: '{featureName} feature privilege', + values: { + featureName: feature.name, + }, + })} + style={{ + minWidth: 200, + }} /> ); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx index 5432a50c1f0df..41e15387f9c47 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiCheckbox, EuiButtonGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { NO_PRIVILEGE_VALUE } from '../constants'; import { PrivilegeFormCalculator } from '../privilege_form_calculator'; import { @@ -118,7 +119,7 @@ export const SubFeatureForm = (props: Props) => { options={options} idSelected={firstSelectedPrivilege?.id ?? NO_PRIVILEGE_VALUE} isDisabled={props.disabled} - onChange={(selectedPrivilegeId) => { + onChange={(selectedPrivilegeId: string) => { // Deselect all privileges which belong to this mutually-exclusive group const privilegesWithoutGroupEntries = props.selectedFeaturePrivileges.filter( (sp) => !privilegeGroup.privileges.some((privilege) => privilege.id === sp) @@ -130,6 +131,15 @@ export const SubFeatureForm = (props: Props) => { props.onChange([...privilegesWithoutGroupEntries, selectedPrivilegeId]); } }} + legend={i18n.translate( + 'xpack.security.management.editRole.subFeatureForm.controlLegendText', + { + defaultMessage: '{subFeatureName} sub-feature privilege', + values: { + subFeatureName: props.subFeature.name, + }, + } + )} /> ); } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx index 6601c6ae1f8d5..8254e2632aa9d 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx @@ -193,7 +193,7 @@ describe('', () => { const featurePrivilegeToggles = wrapper.find(EuiButtonGroup); expect(featurePrivilegeToggles).toHaveLength(1); - expect(featurePrivilegeToggles.find('button')).toHaveLength(3); + expect(featurePrivilegeToggles.find('input')).toHaveLength(3); (featurePrivilegeToggles.props() as EuiButtonGroupProps).onChange('feature1_all', null); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index 28bbd55c7d544..7ce878321185a 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -222,6 +222,12 @@ export class PrivilegeSpaceForm extends Component { idSelected={this.getDisplayedBasePrivilege()} isDisabled={!hasSelectedSpaces} onChange={this.onSpaceBasePrivilegeChange} + legend={i18n.translate( + 'xpack.security.management.editRole.spacePrivilegeForm.basePrivilegeControlLegend', + { + defaultMessage: 'Privileges for all features', + } + )} /> diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 2910f02a187f4..767a2616a4c7e 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -179,3 +179,5 @@ export const showAllOthersBucket: string[] = [ 'destination.ip', 'user.name', ]; + +export const ENABLE_NEW_TIMELINE = false; diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 491f4f8952fd9..783f8be840b7f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -55,7 +55,6 @@ import { MITRE_ATTACK_DETAILS, REFERENCE_URLS_DETAILS, RISK_SCORE_DETAILS, - RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, RULE_TYPE_DETAILS, RUNS_EVERY_DETAILS, @@ -180,7 +179,7 @@ describe('Custom detection rules creation', () => { getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); @@ -333,9 +332,7 @@ describe('Custom detection rules deletion and edition', () => { getDetails(RISK_SCORE_DETAILS).should('have.text', editedRule.riskScore); getDetails(TAGS_DETAILS).should('have.text', expectedEditedtags); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE) - .eq(INVESTIGATION_NOTES_TOGGLE) - .click({ force: true }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', editedRule.note); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should( diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts index bee4713ca7cda..3d4aaca8bb78f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts @@ -38,7 +38,6 @@ import { MITRE_ATTACK_DETAILS, REFERENCE_URLS_DETAILS, RISK_SCORE_DETAILS, - RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, RULE_TYPE_DETAILS, RUNS_EVERY_DETAILS, @@ -142,7 +141,7 @@ describe('Detection rules, EQL', () => { getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index e31fe2e9a3911..e905365d1bbb3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -41,7 +41,6 @@ import { REFERENCE_URLS_DETAILS, RISK_SCORE_DETAILS, RISK_SCORE_OVERRIDE_DETAILS, - RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, RULE_NAME_OVERRIDE_DETAILS, RULE_TYPE_DETAILS, @@ -160,7 +159,7 @@ describe('Detection rules, override', () => { }); }); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts index a6f974256f3e4..a9b43d82bb7fd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -38,7 +38,6 @@ import { MITRE_ATTACK_DETAILS, REFERENCE_URLS_DETAILS, RISK_SCORE_DETAILS, - RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, RULE_TYPE_DETAILS, RUNS_EVERY_DETAILS, @@ -141,7 +140,7 @@ describe('Detection rules, threshold', () => { getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index e40b81ed0e856..d72210dd3e083 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -34,7 +34,7 @@ export const INDEX_PATTERNS_DETAILS = 'Index patterns'; export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown'; -export const INVESTIGATION_NOTES_TOGGLE = 1; +export const INVESTIGATION_NOTES_TOGGLE = '[data-test-subj="stepAboutDetailsToggle-notes"]'; export const MACHINE_LEARNING_JOB_ID = '[data-test-subj="machineLearningJobId"]'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx index a37c9052c2ff3..2d3a7850eb0b6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_status/index.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import styled, { css } from 'styled-components'; import { EuiBadge, + EuiButton, EuiButtonEmpty, - EuiButtonToggle, EuiDescriptionList, EuiDescriptionListDescription, EuiDescriptionListTitle, @@ -44,7 +44,7 @@ interface CaseStatusProps { onRefresh: () => void; status: string; title: string; - toggleStatusCase: (evt: unknown) => void; + toggleStatusCase: (status: boolean) => void; value: string | null; } const CaseStatusComp: React.FC = ({ @@ -62,56 +62,62 @@ const CaseStatusComp: React.FC = ({ title, toggleStatusCase, value, -}) => ( - - - - - - {i18n.STATUS} - - - {status} - - +}) => { + const handleToggleStatusCase = useCallback(() => { + toggleStatusCase(!isSelected); + }, [toggleStatusCase, isSelected]); + return ( + + + + + + {i18n.STATUS} + + + {status} + + + + + {title} + + + + + + + + + + + + {i18n.CASE_REFRESH} + - {title} - - - + + {buttonLabel} + + + + - - - - - - - {i18n.CASE_REFRESH} - - - - - - - - - - - -); + + + ); +}; export const CaseStatus = React.memo(CaseStatusComp); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 58d7efb763ee2..5cb6ede0d9d21 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -183,9 +183,7 @@ describe('CaseView ', () => { ); await waitFor(() => { - wrapper - .find('input[data-test-subj="toggle-case-status"]') - .simulate('change', { target: { checked: true } }); + wrapper.find('[data-test-subj="toggle-case-status"]').first().simulate('click'); expect(updateCaseProperty).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 52cea10cfb275..7ee2b856f8786 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -5,7 +5,7 @@ */ import { - EuiButtonToggle, + EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingContent, @@ -242,10 +242,10 @@ export const CaseComponent = React.memo( ); const toggleStatusCase = useCallback( - (e) => + (nextStatus) => onUpdateField({ key: 'status', - value: e.target.checked ? 'closed' : 'open', + value: nextStatus ? 'closed' : 'open', }), [onUpdateField] ); @@ -307,6 +307,11 @@ export const CaseComponent = React.memo( [allCasesLink] ); + const isSelected = useMemo(() => caseStatusData.isSelected, [caseStatusData]); + const handleToggleStatusCase = useCallback(() => { + toggleStatusCase(!isSelected); + }, [toggleStatusCase, isSelected]); + return ( <> @@ -330,7 +335,7 @@ export const CaseComponent = React.memo( disabled={!userCanCrud} isLoading={isLoading && updateKey === 'status'} onRefresh={handleRefresh} - toggleStatusCase={toggleStatusCase} + toggleStatusCase={handleToggleStatusCase} {...caseStatusData} /> @@ -358,15 +363,16 @@ export const CaseComponent = React.memo( - + fill={caseStatusData.isSelected} + onClick={handleToggleStatusCase} + > + {caseStatusData.buttonLabel} + {hasDataToPush && ( diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap index 3df8663324fdd..6331a2e02b219 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -77,15 +77,6 @@ exports[`Paginated Table Component rendering it renders the default load more ta "warning": "#ffce7a", }, "euiButtonMinWidth": "112px", - "euiButtonToggleBorderColor": "#343741", - "euiButtonToggleTypes": Object { - "danger": "#ff6666", - "ghost": "#ffffff", - "primary": "#1ba9f5", - "secondary": "#7de2d1", - "text": "#98a2b3", - "warning": "#ffce7a", - }, "euiButtonTypes": Object { "danger": "#ff6666", "ghost": "#ffffff", @@ -340,15 +331,6 @@ exports[`Paginated Table Component rendering it renders the default load more ta "small": "14px", "xSmall": "12px", }, - "euiNavDrawerBackgroundColor": "#1d1e24", - "euiNavDrawerContractingDelay": "150ms", - "euiNavDrawerExpandingDelay": "250ms", - "euiNavDrawerExtendedDelay": "1000ms", - "euiNavDrawerMenuAddedDelay": "90ms", - "euiNavDrawerSideShadow": "2px 0 2px -1px rgba(0, 0, 0, 0.3)", - "euiNavDrawerTopPosition": "49px", - "euiNavDrawerWidthCollapsed": "48px", - "euiNavDrawerWidthExpanded": "240px", "euiPageBackgroundColor": "#1a1b20", "euiPaletteColorBlind": Object { "euiColorVis0": Object { @@ -402,10 +384,22 @@ exports[`Paginated Table Component rendering it renders the default load more ta "euiPopoverTranslateDistance": "8px", "euiProgressColors": Object { "accent": "#f990c0", + "customColor": "currentColor", "danger": "#ff6666", "primary": "#1ba9f5", "secondary": "#7de2d1", "subdued": "#81858f", + "success": "#7de2d1", + "vis0": "#54b399", + "vis1": "#6092c0", + "vis2": "#d36086", + "vis3": "#9170b8", + "vis4": "#ca8eae", + "vis5": "#d6bf57", + "vis6": "#b9a888", + "vis7": "#da8b45", + "vis8": "#aa6556", + "vis9": "#e7664c", "warning": "#ffce7a", }, "euiProgressSizes": Object { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx index 757319e7aa1ae..555e02d13d723 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx @@ -104,8 +104,8 @@ describe('StepAboutRuleToggleDetails', () => { ); expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy(); - expect(wrapper.find('EuiButtonToggle[id="details"]').at(0).prop('isSelected')).toBeTruthy(); - expect(wrapper.find('EuiButtonToggle[id="notes"]').at(0).prop('isSelected')).toBeFalsy(); + expect(wrapper.find('#details').at(0).prop('isSelected')).toBeTruthy(); + expect(wrapper.find('#notes').at(0).prop('isSelected')).toBeFalsy(); }); test('it allows users to toggle between "details" and "note"', () => { @@ -122,16 +122,17 @@ describe('StepAboutRuleToggleDetails', () => { ); - expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeTruthy(); - expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeFalsy(); + expect(wrapper.find('[idSelected="details"]').exists()).toBeTruthy(); + expect(wrapper.find('[idSelected="notes"]').exists()).toBeFalsy(); wrapper - .find('input[title="Investigation guide"]') + .find('[title="Investigation guide"]') .at(0) + .find('input') .simulate('change', { target: { value: 'notes' } }); - expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeFalsy(); - expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); + expect(wrapper.find('[idSelected="details"]').exists()).toBeFalsy(); + expect(wrapper.find('[idSelected="notes"]').exists()).toBeTruthy(); }); test('it displays notes markdown when user toggles to "notes"', () => { @@ -149,8 +150,9 @@ describe('StepAboutRuleToggleDetails', () => { ); wrapper - .find('input[title="Investigation guide"]') + .find('[title="Investigation guide"]') .at(0) + .find('input') .simulate('change', { target: { value: 'notes' } }); expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx index 52e9dc7e44ff7..fb98233bf8cc7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiProgress, EuiButtonGroup, - EuiButtonGroupOption, + EuiButtonGroupOptionProps, EuiSpacer, EuiFlexItem, EuiText, @@ -46,14 +46,16 @@ const AboutContent = styled.div` height: 100%; `; -const toggleOptions: EuiButtonGroupOption[] = [ +const toggleOptions: EuiButtonGroupOptionProps[] = [ { id: 'details', label: i18n.ABOUT_PANEL_DETAILS_TAB, + 'data-test-subj': 'stepAboutDetailsToggle-details', }, { id: 'notes', label: i18n.ABOUT_PANEL_NOTES_TAB, + 'data-test-subj': 'stepAboutDetailsToggle-notes', }, ]; @@ -98,6 +100,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ setToggleOption(val); }} data-test-subj="stepAboutDetailsToggle" + legend={i18n.ABOUT_CONTROL_LEGEND} /> )} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts index e1b89b1ec8ce2..8a98697b523d7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts @@ -25,3 +25,10 @@ export const ABOUT_PANEL_NOTES_TAB = i18n.translate( defaultMessage: 'Investigation guide', } ); + +export const ABOUT_CONTROL_LEGEND = i18n.translate( + 'xpack.securitySolution.detectionEngine.details.stepAboutRule.controlLegend', + { + defaultMessage: 'Viewing', + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index 86cb203671ac2..c82b9cac8ab1f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -2653,7 +2653,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" >
); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/filters/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/filters/index.tsx index af2c1523605d2..1bf608787fd7e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/filters/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/filters/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonGroup, EuiButtonGroupOption } from '@elastic/eui'; +import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { FilterMode } from '../types'; @@ -13,7 +13,7 @@ import * as i18n from '../translations'; const MY_RECENTLY_REPORTED_ID = 'myRecentlyReported'; -const toggleButtonIcons: EuiButtonGroupOption[] = [ +const toggleButtonIcons: EuiButtonGroupOptionProps[] = [ { id: 'recentlyCreated', label: i18n.RECENTLY_CREATED_CASES, @@ -45,7 +45,15 @@ export const Filters = React.memo<{ [setFilterBy] ); - return ; + return ( + + ); }); Filters.displayName = 'Filters'; diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/translations.ts b/x-pack/plugins/security_solution/public/overview/components/recent_cases/translations.ts index f9b3e05ad9595..ff5585affb475 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/translations.ts @@ -41,3 +41,10 @@ export const VIEW_ALL_CASES = i18n.translate( defaultMessage: 'View all cases', } ); + +export const CASES_FILTER_CONTROL = i18n.translate( + 'xpack.securitySolution.recentCases.controlLegend', + { + defaultMessage: 'Cases filter', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/filters/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/filters/index.tsx index 815768482781b..bd6f1271f3073 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/filters/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/filters/index.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonGroup, EuiButtonGroupOption } from '@elastic/eui'; +import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui'; import React from 'react'; import { FilterMode } from '../types'; import * as i18n from '../translations'; -const toggleButtonIcons: EuiButtonGroupOption[] = [ +const toggleButtonIcons: EuiButtonGroupOptionProps[] = [ { id: 'favorites', label: i18n.FAVORITES, @@ -35,6 +35,7 @@ export const Filters = React.memo<{ setFilterBy(f as FilterMode); }} isIconOnly + legend={i18n.TIMELINES_FILTER_CONTROL} /> )); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/translations.ts b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/translations.ts index 468773ae90790..998e333d727d2 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/translations.ts @@ -81,3 +81,10 @@ export const VIEW_ALL_TIMELINES = i18n.translate( defaultMessage: 'View all timelines', } ); + +export const TIMELINES_FILTER_CONTROL = i18n.translate( + 'xpack.securitySolution.recentTimelines.filterControlLegend', + { + defaultMessage: 'Timelines filter', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index a711e7a1d0442..0737db7a00788 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -125,13 +125,27 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), - updateDescription: ({ id, description }: { id: string; description: string }) => - dispatch(timelineActions.updateDescription({ id, description })), + updateDescription: ({ + id, + description, + disableAutoSave, + }: { + id: string; + description: string; + disableAutoSave?: boolean; + }) => dispatch(timelineActions.updateDescription({ id, description, disableAutoSave })), updateIsFavorite: ({ id, isFavorite }: { id: string; isFavorite: boolean }) => dispatch(timelineActions.updateIsFavorite({ id, isFavorite })), updateNote: (note: Note) => dispatch(appActions.updateNote({ note })), - updateTitle: ({ id, title }: { id: string; title: string }) => - dispatch(timelineActions.updateTitle({ id, title })), + updateTitle: ({ + id, + title, + disableAutoSave, + }: { + id: string; + title: string; + disableAutoSave?: boolean; + }) => dispatch(timelineActions.updateTitle({ id, title, disableAutoSave })), toggleLock: ({ linkToId }: { linkToId: InputsModelId }) => dispatch(inputsActions.toggleTimelineLinkTo({ linkToId })), }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap index 10ad0123f7fc6..17c614bd2c83c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap @@ -77,15 +77,6 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "warning": "#ffce7a", }, "euiButtonMinWidth": "112px", - "euiButtonToggleBorderColor": "#343741", - "euiButtonToggleTypes": Object { - "danger": "#ff6666", - "ghost": "#ffffff", - "primary": "#1ba9f5", - "secondary": "#7de2d1", - "text": "#98a2b3", - "warning": "#ffce7a", - }, "euiButtonTypes": Object { "danger": "#ff6666", "ghost": "#ffffff", @@ -340,15 +331,6 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "small": "14px", "xSmall": "12px", }, - "euiNavDrawerBackgroundColor": "#1d1e24", - "euiNavDrawerContractingDelay": "150ms", - "euiNavDrawerExpandingDelay": "250ms", - "euiNavDrawerExtendedDelay": "1000ms", - "euiNavDrawerMenuAddedDelay": "90ms", - "euiNavDrawerSideShadow": "2px 0 2px -1px rgba(0, 0, 0, 0.3)", - "euiNavDrawerTopPosition": "49px", - "euiNavDrawerWidthCollapsed": "48px", - "euiNavDrawerWidthExpanded": "240px", "euiPageBackgroundColor": "#1a1b20", "euiPaletteColorBlind": Object { "euiColorVis0": Object { @@ -402,10 +384,22 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "euiPopoverTranslateDistance": "8px", "euiProgressColors": Object { "accent": "#f990c0", + "customColor": "currentColor", "danger": "#ff6666", "primary": "#1ba9f5", "secondary": "#7de2d1", "subdued": "#81858f", + "success": "#7de2d1", + "vis0": "#54b399", + "vis1": "#6092c0", + "vis2": "#d36086", + "vis3": "#9170b8", + "vis4": "#ca8eae", + "vis5": "#d6bf57", + "vis6": "#b9a888", + "vis7": "#da8b45", + "vis8": "#aa6556", + "vis9": "#e7664c", "warning": "#ffce7a", }, "euiProgressSizes": Object { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx index 0cd7032596f15..ff3df357f7337 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx @@ -196,7 +196,6 @@ const AddDataProviderPopoverComponent: React.FC = ( isOpen={isAddFilterPopoverOpen} closePopover={handleClosePopover} anchorPosition="downLeft" - withTitle panelPaddingSize="none" ownFocus={true} repositionOnScroll diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx new file mode 100644 index 0000000000000..e9dc312ee8d19 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { SaveTimelineButton } from './save_timeline_button'; +import { act } from '@testing-library/react-hooks'; + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn(), + }; +}); + +jest.mock('./title_and_description'); + +describe('SaveTimelineButton', () => { + const props = { + timelineId: 'timeline-1', + showOverlay: false, + toolTip: 'tooltip message', + toggleSaveTimeline: jest.fn(), + onSaveTimeline: jest.fn(), + updateTitle: jest.fn(), + updateDescription: jest.fn(), + }; + test('Show tooltip', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(true); + }); + + test('Hide tooltip', () => { + const testProps = { + ...props, + showOverlay: true, + }; + const component = mount(); + component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click'); + + act(() => { + expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual( + false + ); + }); + }); + + test('should show a button with pencil icon', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-button-icon"]').prop('iconType')).toEqual( + 'pencil' + ); + }); + + test('should not show a modal when showOverlay equals false', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(false); + }); + + test('should show a modal when showOverlay equals true', () => { + const testProps = { + ...props, + showOverlay: true, + }; + const component = mount(); + component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click'); + act(() => { + expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx new file mode 100644 index 0000000000000..476ef8d1dd5a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonIcon, EuiOverlayMask, EuiModal, EuiToolTip } from '@elastic/eui'; + +import React, { useCallback, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { timelineActions } from '../../../store/timeline'; +import { NOTES_PANEL_WIDTH } from '../properties/notes_size'; + +import { TimelineTitleAndDescription } from './title_and_description'; +import { EDIT } from './translations'; + +export interface SaveTimelineComponentProps { + timelineId: string; + toolTip?: string; +} + +export const SaveTimelineButton = React.memo( + ({ timelineId, toolTip }) => { + const [showSaveTimelineOverlay, setShowSaveTimelineOverlay] = useState(false); + const onToggleSaveTimeline = useCallback(() => { + setShowSaveTimelineOverlay((prevShowSaveTimelineOverlay) => !prevShowSaveTimelineOverlay); + }, [setShowSaveTimelineOverlay]); + + const dispatch = useDispatch(); + const updateTitle = useCallback( + ({ id, title, disableAutoSave }: { id: string; title: string; disableAutoSave?: boolean }) => + dispatch(timelineActions.updateTitle({ id, title, disableAutoSave })), + [dispatch] + ); + + const updateDescription = useCallback( + ({ + id, + description, + disableAutoSave, + }: { + id: string; + description: string; + disableAutoSave?: boolean; + }) => dispatch(timelineActions.updateDescription({ id, description, disableAutoSave })), + [dispatch] + ); + + const saveTimelineButtonIcon = useMemo( + () => ( + + ), + [onToggleSaveTimeline] + ); + + return showSaveTimelineOverlay ? ( + <> + {saveTimelineButtonIcon} + + + + + + + ) : ( + + {saveTimelineButtonIcon} + + ); + } +); + +SaveTimelineButton.displayName = 'SaveTimelineButton'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx new file mode 100644 index 0000000000000..bcc90a25d5789 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { TimelineTitleAndDescription } from './title_and_description'; +import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { useCreateTimelineButton } from '../properties/use_create_timeline'; +import { TimelineType } from '../../../../../common/types/timeline'; +import * as i18n from './translations'; + +jest.mock('../../../../common/hooks/use_selector', () => ({ + useShallowEqualSelector: jest.fn(), +})); + +jest.mock('../../../../timelines/store/timeline', () => ({ + timelineSelectors: { + selectTimeline: jest.fn(), + }, +})); + +jest.mock('../properties/use_create_timeline', () => ({ + useCreateTimelineButton: jest.fn(), +})); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn(), + }; +}); + +describe('TimelineTitleAndDescription', () => { + describe('save timeline', () => { + const props = { + timelineId: 'timeline-1', + toggleSaveTimeline: jest.fn(), + onSaveTimeline: jest.fn(), + updateTitle: jest.fn(), + updateDescription: jest.fn(), + }; + + const mockGetButton = jest.fn().mockReturnValue(
); + + beforeEach(() => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: '', + isSaving: true, + savedObjectId: null, + title: 'my timeline', + timelineType: TimelineType.default, + }); + (useCreateTimelineButton as jest.Mock).mockReturnValue({ + getButton: mockGetButton, + }); + }); + + afterEach(() => { + (useShallowEqualSelector as jest.Mock).mockReset(); + (useCreateTimelineButton as jest.Mock).mockReset(); + mockGetButton.mockClear(); + }); + + test('show proress bar while saving', () => { + const component = shallow(); + expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true); + }); + + test('Show correct header for save timeline modal', () => { + const component = shallow(); + expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + i18n.SAVE_TIMELINE + ); + }); + + test('Show correct header for save timeline template modal', () => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: '', + isSaving: true, + savedObjectId: null, + title: 'my timeline', + timelineType: TimelineType.template, + }); + const component = shallow(); + expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + i18n.SAVE_TIMELINE_TEMPLATE + ); + }); + + test('Show name field', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-name"]').exists()).toEqual(true); + }); + + test('Show description field', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true); + }); + + test('Show close button', () => { + const component = shallow(); + expect(component.find('[data-test-subj="close-button"]').exists()).toEqual(true); + }); + + test('Show saveButton', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true); + }); + }); + + describe('update timeline', () => { + const props = { + timelineId: 'timeline-1', + toggleSaveTimeline: jest.fn(), + onSaveTimeline: jest.fn(), + updateTitle: jest.fn(), + updateDescription: jest.fn(), + }; + + const mockGetButton = jest.fn().mockReturnValue(
); + + beforeEach(() => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: 'xxxx', + isSaving: true, + savedObjectId: '1234', + title: 'my timeline', + timelineType: TimelineType.default, + }); + (useCreateTimelineButton as jest.Mock).mockReturnValue({ + getButton: mockGetButton, + }); + }); + + afterEach(() => { + (useShallowEqualSelector as jest.Mock).mockReset(); + (useCreateTimelineButton as jest.Mock).mockReset(); + mockGetButton.mockClear(); + }); + + test('show proress bar while saving', () => { + const component = shallow(); + expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true); + }); + + test('Show correct header for save timeline modal', () => { + const component = shallow(); + expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + i18n.NAME_TIMELINE + ); + }); + + test('Show correct header for save timeline template modal', () => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: 'xxxx', + isSaving: true, + savedObjectId: '1234', + title: 'my timeline', + timelineType: TimelineType.template, + }); + const component = shallow(); + expect(component.find('[data-test-subj="modal-header"]').prop('children')).toEqual( + i18n.NAME_TIMELINE_TEMPLATE + ); + }); + + test('Show name field', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-name"]').exists()).toEqual(true); + }); + + test('Show description field', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-description"]').exists()).toEqual(true); + }); + + test('Show saveButton', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true); + }); + }); + + describe('showWarning', () => { + const props = { + timelineId: 'timeline-1', + toggleSaveTimeline: jest.fn(), + onSaveTimeline: jest.fn(), + updateTitle: jest.fn(), + updateDescription: jest.fn(), + showWarning: true, + }; + + const mockGetButton = jest.fn().mockReturnValue(
); + + beforeEach(() => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: '', + isSaving: true, + savedObjectId: null, + title: 'my timeline', + timelineType: TimelineType.default, + showWarnging: true, + }); + (useCreateTimelineButton as jest.Mock).mockReturnValue({ + getButton: mockGetButton, + }); + }); + + afterEach(() => { + (useShallowEqualSelector as jest.Mock).mockReset(); + (useCreateTimelineButton as jest.Mock).mockReset(); + mockGetButton.mockClear(); + }); + + test('Show EuiCallOut', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-timeline-callout"]').exists()).toEqual(true); + }); + + test('Show discardTimelineButton', () => { + const component = shallow(); + expect(component.find('[data-test-subj="mock-discard-button"]').exists()).toEqual(true); + }); + + test('get discardTimelineButton with correct props', () => { + shallow(); + expect(mockGetButton).toBeCalledWith({ + title: i18n.DISCARD_TIMELINE, + outline: true, + iconType: '', + fill: false, + }); + }); + + test('get discardTimelineTemplateButton with correct props', () => { + (useShallowEqualSelector as jest.Mock).mockReturnValue({ + description: 'xxxx', + isSaving: true, + savedObjectId: null, + title: 'my timeline', + timelineType: TimelineType.template, + }); + shallow(); + expect(mockGetButton).toBeCalledWith({ + title: i18n.DISCARD_TIMELINE_TEMPLATE, + outline: true, + iconType: '', + fill: false, + }); + }); + + test('Show saveButton', () => { + const component = shallow(); + expect(component.find('[data-test-subj="save-button"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx new file mode 100644 index 0000000000000..3597b26e2663a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiFlexGroup, + EuiFormRow, + EuiFlexItem, + EuiModalBody, + EuiModalHeader, + EuiSpacer, + EuiProgress, + EuiCallOut, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { TimelineType } from '../../../../../common/types/timeline'; +import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; +import { TimelineInput } from '../../../store/timeline/actions'; +import { Description, Name, UpdateTitle, UpdateDescription } from '../properties/helpers'; +import { TIMELINE_TITLE, DESCRIPTION, OPTIONAL } from '../properties/translations'; +import { useCreateTimelineButton } from '../properties/use_create_timeline'; +import * as i18n from './translations'; + +interface TimelineTitleAndDescriptionProps { + showWarning?: boolean; + timelineId: string; + toggleSaveTimeline: () => void; + updateTitle: UpdateTitle; + updateDescription: UpdateDescription; +} + +const Wrapper = styled(EuiModalBody)` + .euiFormRow { + max-width: none; + } + + .euiFormControlLayout { + max-width: none; + } + + .euiFieldText { + max-width: none; + } +`; + +Wrapper.displayName = 'Wrapper'; + +const usePrevious = (value: unknown) => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; + +// when showWarning equals to true, +// the modal is used as a reminder for users to save / discard +// the unsaved timeline / template +export const TimelineTitleAndDescription = React.memo( + ({ timelineId, toggleSaveTimeline, updateTitle, updateDescription, showWarning }) => { + const timeline = useShallowEqualSelector((state) => + timelineSelectors.selectTimeline(state, timelineId) + ); + + const { description, isSaving, savedObjectId, title, timelineType } = timeline; + + const prevIsSaving = usePrevious(isSaving); + const dispatch = useDispatch(); + const onSaveTimeline = useCallback( + (args: TimelineInput) => dispatch(timelineActions.saveTimeline(args)), + [dispatch] + ); + + const handleClick = useCallback(() => { + onSaveTimeline({ + ...timeline, + id: timelineId, + }); + }, [onSaveTimeline, timeline, timelineId]); + + const { getButton } = useCreateTimelineButton({ timelineId, timelineType }); + + const discardTimelineButton = useMemo( + () => + getButton({ + title: + timelineType === TimelineType.template + ? i18n.DISCARD_TIMELINE_TEMPLATE + : i18n.DISCARD_TIMELINE, + outline: true, + iconType: '', + fill: false, + }), + [getButton, timelineType] + ); + + useEffect(() => { + if (!isSaving && prevIsSaving) { + toggleSaveTimeline(); + } + }, [isSaving, prevIsSaving, toggleSaveTimeline]); + + const modalHeader = + savedObjectId == null + ? timelineType === TimelineType.template + ? i18n.SAVE_TIMELINE_TEMPLATE + : i18n.SAVE_TIMELINE + : timelineType === TimelineType.template + ? i18n.NAME_TIMELINE_TEMPLATE + : i18n.NAME_TIMELINE; + + const saveButtonTitle = + savedObjectId == null && showWarning + ? timelineType === TimelineType.template + ? i18n.SAVE_TIMELINE_TEMPLATE + : i18n.SAVE_TIMELINE + : i18n.SAVE; + + const calloutMessage = useMemo(() => i18n.UNSAVED_TIMELINE_WARNING(timelineType), [ + timelineType, + ]); + + const descriptionLabel = savedObjectId == null ? `${DESCRIPTION} (${OPTIONAL})` : DESCRIPTION; + + return ( + <> + {isSaving && ( + + )} + {modalHeader} + + + {showWarning && ( + + + + + )} + + + + + + + + + + + + + + + + {savedObjectId == null && showWarning ? ( + discardTimelineButton + ) : ( + + {i18n.CLOSE_MODAL} + + )} + + + + {saveButtonTitle} + + + + + + + ); + } +); + +TimelineTitleAndDescription.displayName = 'TimelineTitleAndDescription'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts index 89ad11d75cae1..80aa719a3469d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { TimelineType, TimelineTypeLiteral } from '../../../../../common/types/timeline'; export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate( 'xpack.securitySolution.timeline.callOut.unauthorized.message.description', @@ -21,3 +22,66 @@ export const CALL_OUT_IMMUTABLE = i18n.translate( 'This prebuilt timeline template cannot be modified. To make changes, please duplicate this template and make modifications to the duplicate template.', } ); + +export const EDIT = i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.button', { + defaultMessage: 'edit', +}); + +export const SAVE_TIMELINE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.header', + { + defaultMessage: 'Save Timeline', + } +); + +export const SAVE_TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.header', + { + defaultMessage: 'Save Timeline Template', + } +); + +export const SAVE = i18n.translate('xpack.securitySolution.timeline.nameTimeline.save.title', { + defaultMessage: 'Save', +}); + +export const NAME_TIMELINE = i18n.translate( + 'xpack.securitySolution.timeline.nameTimeline.modal.header', + { + defaultMessage: 'Name Timeline', + } +); + +export const NAME_TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.nameTimelineTemplate.modal.header', + { + defaultMessage: 'Name Timeline Template', + } +); + +export const DISCARD_TIMELINE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.discard.title', + { + defaultMessage: 'Discard Timeline', + } +); + +export const DISCARD_TIMELINE_TEMPLATE = i18n.translate( + 'xpack.securitySolution.timeline.saveTimelineTemplate.modal.discard.title', + { + defaultMessage: 'Discard Timeline Template', + } +); + +export const CLOSE_MODAL = i18n.translate( + 'xpack.securitySolution.timeline.saveTimeline.modal.close.title', + { + defaultMessage: 'Close', + } +); + +export const UNSAVED_TIMELINE_WARNING = (timelineType: TimelineTypeLiteral) => + i18n.translate('xpack.securitySolution.timeline.saveTimeline.modal.warning.title', { + values: { timeline: timelineType === TimelineType.template ? 'timeline template' : 'timeline' }, + defaultMessage: 'You have an unsaved {timeline}. Do you wish to save it?', + }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx index 887c2e1e825f8..dd0695e795397 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx @@ -5,8 +5,10 @@ */ import React from 'react'; import { mount, shallow } from 'enzyme'; -import { NewTimeline, NewTimelineProps } from './helpers'; +import { Description, Name, NewTimeline, NewTimelineProps } from './helpers'; import { useCreateTimelineButton } from './use_create_timeline'; +import * as i18n from './translations'; +import { TimelineType } from '../../../../../common/types/timeline'; jest.mock('./use_create_timeline', () => ({ useCreateTimelineButton: jest.fn(), @@ -83,3 +85,72 @@ describe('NewTimeline', () => { }); }); }); + +describe('Description', () => { + const props = { + description: 'xxx', + timelineId: 'timeline-1', + updateDescription: jest.fn(), + }; + + test('should render tooltip', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="timeline-description-tool-tip"]').prop('content') + ).toEqual(i18n.DESCRIPTION_TOOL_TIP); + }); + + test('should not render textarea if isTextArea is false', () => { + const component = shallow(); + expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual( + false + ); + + expect(component.find('[data-test-subj="timeline-description"]').exists()).toEqual(true); + }); + + test('should render textarea if isTextArea is true', () => { + const testProps = { + ...props, + isTextArea: true, + }; + const component = shallow(); + expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual( + true + ); + }); +}); + +describe('Name', () => { + const props = { + timelineId: 'timeline-1', + timelineType: TimelineType.default, + title: 'xxx', + updateTitle: jest.fn(), + }; + + test('should render tooltip', () => { + const component = shallow(); + expect(component.find('[data-test-subj="timeline-title-tool-tip"]').prop('content')).toEqual( + i18n.TITLE + ); + }); + + test('should render placeholder by timelineType - timeline', () => { + const component = shallow(); + expect(component.find('[data-test-subj="timeline-title"]').prop('placeholder')).toEqual( + i18n.UNTITLED_TIMELINE + ); + }); + + test('should render placeholder by timelineType - timeline template', () => { + const testProps = { + ...props, + timelineType: TimelineType.template, + }; + const component = shallow(); + expect(component.find('[data-test-subj="timeline-title"]').prop('placeholder')).toEqual( + i18n.UNTITLED_TEMPLATE + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index a28f4240d3a2f..25039dbc9529a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -16,8 +16,9 @@ import { EuiModal, EuiOverlayMask, EuiToolTip, + EuiTextArea, } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; @@ -41,14 +42,22 @@ import { Notes } from '../../notes'; import { AssociateNote, UpdateNote } from '../../notes/helpers'; import { NOTES_PANEL_WIDTH } from './notes_size'; -import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; +import { + ButtonContainer, + DescriptionContainer, + LabelText, + NameField, + NameWrapper, + StyledStar, +} from './styles'; import * as i18n from './translations'; -import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions'; +import { setInsertTimeline, showTimeline, TimelineInput } from '../../../store/timeline/actions'; import { useCreateTimelineButton } from './use_create_timeline'; export const historyToolTip = 'The chronological history of actions related to this timeline'; export const streamLiveToolTip = 'Update the Timeline as new data arrives'; export const newTimelineToolTip = 'Create a new timeline'; +export const TIMELINE_TITLE_CLASSNAME = 'timeline-title'; const NotesCountBadge = (styled(EuiBadge)` margin-left: 5px; @@ -66,8 +75,25 @@ type CreateTimeline = ({ timelineType?: TimelineTypeLiteral; }) => void; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; +export type UpdateTitle = ({ + id, + title, + disableAutoSave, +}: { + id: string; + title: string; + disableAutoSave?: boolean; +}) => void; +export type UpdateDescription = ({ + id, + description, + disableAutoSave, +}: { + id: string; + description: string; + disableAutoSave?: boolean; +}) => void; +export type SaveTimeline = (args: TimelineInput) => void; export const StarIcon = React.memo<{ isFavorite: boolean; @@ -104,55 +130,146 @@ interface DescriptionProps { description: string; timelineId: string; updateDescription: UpdateDescription; + isTextArea?: boolean; + disableAutoSave?: boolean; + disableTooltip?: boolean; + disabled?: boolean; + marginRight?: number; } export const Description = React.memo( - ({ description, timelineId, updateDescription }) => ( - - - updateDescription({ id: timelineId, description: e.target.value })} - placeholder={i18n.DESCRIPTION} - spellCheck={true} - value={description} - /> + ({ + description, + timelineId, + updateDescription, + isTextArea = false, + disableAutoSave = false, + disableTooltip = false, + disabled = false, + marginRight, + }) => { + const onDescriptionChanged = useCallback( + (e) => { + updateDescription({ id: timelineId, description: e.target.value, disableAutoSave }); + }, + [updateDescription, disableAutoSave, timelineId] + ); + + const inputField = useMemo( + () => + isTextArea ? ( + + ) : ( + + ), + [description, isTextArea, onDescriptionChanged, disabled] + ); + return ( + + {disableTooltip ? ( + inputField + ) : ( + + {inputField} + + )} - - ) + ); + } ); Description.displayName = 'Description'; interface NameProps { + autoFocus?: boolean; + disableAutoSave?: boolean; + disableTooltip?: boolean; + disabled?: boolean; timelineId: string; timelineType: TimelineType; title: string; updateTitle: UpdateTitle; + width?: string; + marginRight?: number; } -export const Name = React.memo(({ timelineId, timelineType, title, updateTitle }) => { - const handleChange = useCallback((e) => updateTitle({ id: timelineId, title: e.target.value }), [ +export const Name = React.memo( + ({ + autoFocus = false, + disableAutoSave = false, + disableTooltip = false, + disabled = false, timelineId, + timelineType, + title, updateTitle, - ]); + width, + marginRight, + }) => { + const timelineNameRef = useRef(null); + + const handleChange = useCallback( + (e) => updateTitle({ id: timelineId, title: e.target.value, disableAutoSave }), + [timelineId, updateTitle, disableAutoSave] + ); - return ( - - - - ); -}); + useEffect(() => { + if (autoFocus && timelineNameRef && timelineNameRef.current) { + timelineNameRef.current.focus(); + } + }, [autoFocus]); + + const nameField = useMemo( + () => ( + + ), + [handleChange, marginRight, timelineType, title, width, disabled] + ); + + return ( + + {disableTooltip ? ( + nameField + ) : ( + + {nameField} + + )} + + ); + } +); Name.displayName = 'Name'; interface NewCaseProps { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index 19344a7fd7c9b..cdedca23e85af 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -92,6 +92,7 @@ const defaultProps = { description: '', getNotesByIds: jest.fn(), noteIds: [], + saveTimeline: jest.fn(), status: TimelineStatus.active, timelineId: 'abc', toggleLock: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index 9eea95a0a9b1a..9df2b585449a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -92,6 +92,7 @@ export const Properties = React.memo( setShowTimelineModal(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId }); const datePickerWidth = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx index a3cd8802c36bc..6b181a5af7bf3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx @@ -16,6 +16,8 @@ import { SuperDatePicker } from '../../../../common/components/super_date_picker import { TimelineTypeLiteral, TimelineStatusLiteral } from '../../../../../common/types/timeline'; import * as i18n from './translations'; +import { SaveTimelineButton } from '../header/save_timeline_button'; +import { ENABLE_NEW_TIMELINE } from '../../../../../common/constants'; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; @@ -122,6 +124,8 @@ export const PropertiesLeft = React.memo( ) : null} + {ENABLE_NEW_TIMELINE && } + {showNotesFromWidth ? ( (({ width }) => ({ `; DatePicker.displayName = 'DatePicker'; -export const NameField = styled(EuiFieldText)` - width: 150px; - margin-right: 5px; +export const NameField = styled(({ width, marginRight, ...rest }) => )` + width: ${({ width = '150px' }) => width}; + margin-right: ${({ marginRight = 10 }) => marginRight} px; + + .euiToolTipAnchor { + display: block; + } `; NameField.displayName = 'NameField'; -export const DescriptionContainer = styled.div` +export const NameWrapper = styled.div` + .euiToolTipAnchor { + display: block; + } +`; +NameWrapper.displayName = 'NameWrapper'; + +export const DescriptionContainer = styled.div<{ marginRight?: number }>` animation: ${fadeInEffect} 0.3s; - margin-right: 5px; + margin-right: ${({ marginRight = 5 }) => marginRight}px; min-width: 150px; + + .euiToolTipAnchor { + display: block; + } `; DescriptionContainer.displayName = 'DescriptionContainer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts index 1fc3b7b00f847..78d01b2d98ab3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts @@ -34,7 +34,7 @@ export const NOT_A_FAVORITE = i18n.translate( export const TIMELINE_TITLE = i18n.translate( 'xpack.securitySolution.timeline.properties.timelineTitleAriaLabel', { - defaultMessage: 'Timeline title', + defaultMessage: 'Title', } ); @@ -194,3 +194,10 @@ export const UNLOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( defaultMessage: 'Unlock date picker to global date picker', } ); + +export const OPTIONAL = i18n.translate( + 'xpack.securitySolution.timeline.properties.timelineDescriptionOptional', + { + defaultMessage: 'Optional', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx index c21592bed12e0..10b505da5c76f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx @@ -63,6 +63,46 @@ describe('useCreateTimelineButton', () => { }); }); + test('getButton renders correct iconType - EuiButton', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } + ); + await waitForNextUpdate(); + + const button = result.current.getButton({ + outline: true, + title: 'mock title', + iconType: 'pencil', + }); + const wrapper = shallow(button); + expect(wrapper.find('[data-test-subj="timeline-new-with-border"]').prop('iconType')).toEqual( + 'pencil' + ); + }); + }); + + test('getButton renders correct filling - EuiButton', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } + ); + await waitForNextUpdate(); + + const button = result.current.getButton({ + outline: true, + title: 'mock title', + fill: false, + }); + const wrapper = shallow(button); + expect(wrapper.find('[data-test-subj="timeline-new-with-border"]').prop('fill')).toEqual( + false + ); + }); + }); + test('getButton renders correct outline - EuiButtonEmpty', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index 28dd865c763ae..b4d168cc980b6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -93,15 +93,28 @@ export const useCreateTimelineButton = ({ }, [createTimeline, timelineId, timelineType, closeGearMenu]); const getButton = useCallback( - ({ outline, title }: { outline?: boolean; title?: string }) => { + ({ + outline, + title, + iconType = 'plusInCircle', + fill = true, + isDisabled = false, + }: { + outline?: boolean; + title?: string; + iconType?: string; + fill?: boolean; + isDisabled?: boolean; + }) => { const buttonProps = { - iconType: 'plusInCircle', + iconType, onClick: handleButtonClick, + fill, }; const dataTestSubjPrefix = timelineType === TimelineType.template ? `template-timeline-new` : `timeline-new`; return outline ? ( - + {title} ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 472e82426468e..c066de8af9f20 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -56,7 +56,7 @@ export const applyDeltaToColumnWidth = actionCreator<{ delta: number; }>('APPLY_DELTA_TO_COLUMN_WIDTH'); -export const createTimeline = actionCreator<{ +export interface TimelineInput { id: string; dataProviders?: DataProvider[]; dateRange?: { @@ -76,9 +76,13 @@ export const createTimeline = actionCreator<{ sort?: Sort; showCheckboxes?: boolean; timelineType?: TimelineTypeLiteral; - templateTimelineId?: string; - templateTimelineVersion?: number; -}>('CREATE_TIMELINE'); + templateTimelineId?: string | null; + templateTimelineVersion?: number | null; +} + +export const saveTimeline = actionCreator('SAVE_TIMELINE'); + +export const createTimeline = actionCreator('CREATE_TIMELINE'); export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); @@ -174,9 +178,11 @@ export const updateHighlightedDropAndProviderId = actionCreator<{ providerId: string; }>('UPDATE_DROP_AND_PROVIDER'); -export const updateDescription = actionCreator<{ id: string; description: string }>( - 'UPDATE_DESCRIPTION' -); +export const updateDescription = actionCreator<{ + id: string; + description: string; + disableAutoSave?: boolean; +}>('UPDATE_DESCRIPTION'); export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); @@ -205,7 +211,9 @@ export const updateItemsPerPageOptions = actionCreator<{ itemsPerPageOptions: number[]; }>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); -export const updateTitle = actionCreator<{ id: string; title: string }>('UPDATE_TITLE'); +export const updateTitle = actionCreator<{ id: string; title: string; disableAutoSave?: boolean }>( + 'UPDATE_TITLE' +); export const updatePageIndex = actionCreator<{ id: string; activePage: number }>( 'UPDATE_PAGE_INDEX' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index cc8e856de1b16..d50de33412175 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -78,6 +78,7 @@ import { createTimeline, addTimeline, showCallOutUnauthorizedMsg, + saveTimeline, } from './actions'; import { ColumnHeaderOptions, TimelineModel } from './model'; import { epicPersistNote, timelineNoteActionsType } from './epic_note'; @@ -95,6 +96,7 @@ const timelineActionsType = [ dataProviderEdited.type, removeColumn.type, removeProvider.type, + saveTimeline.type, setExcludedRowRendererIds.type, setFilters.type, setSavedQueryId.type, @@ -179,11 +181,11 @@ export const createTimelineEpic = (): Epic< } else if ( timelineActionsType.includes(action.type) && !timelineObj.isLoading && - isItAtimelineAction(timelineId) + isItAtimelineAction(timelineId) && + !get('payload.disableAutoSave', action) ) { return true; } - return false; }), debounceTime(500), mergeMap(([action]) => { diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 1d956e02e7083..7c227f1c80610 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -389,7 +389,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: updateTimelineKqlMode({ id, kqlMode, timelineById: state.timelineById }), })) - .case(updateTitle, (state, { id, title }) => ({ + .case(updateTitle, (state, { id, title, disableAutoSave }) => ({ ...state, timelineById: updateTimelineTitle({ id, title, timelineById: state.timelineById }), })) diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx index b4612c9df42ff..aecb3c02ef43e 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx @@ -214,7 +214,6 @@ export const PolicyDetails: React.FunctionComponent = ({ isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(false)} panelPaddingSize="none" - withTitle anchorPosition="rightUp" repositionOnScroll > diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_retention_schedule/policy_retention_schedule.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_retention_schedule/policy_retention_schedule.tsx index dc5de0b4295e8..583c8e4ef1dc9 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_retention_schedule/policy_retention_schedule.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_retention_schedule/policy_retention_schedule.tsx @@ -176,7 +176,6 @@ export const PolicyRetentionSchedule: React.FunctionComponent = ({ isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(false)} panelPaddingSize="none" - withTitle anchorPosition="rightUp" repositionOnScroll > diff --git a/x-pack/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap b/x-pack/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap index 22d65f4600e05..e02e81e497806 100644 --- a/x-pack/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap +++ b/x-pack/plugins/spaces/public/nav_control/__snapshots__/nav_control_popover.test.tsx.snap @@ -26,7 +26,6 @@ exports[`NavControlPopover renders without crashing 1`] = ` ownFocus={true} panelPaddingSize="none" repositionOnScroll={true} - withTitle={true} > { anchorPosition={this.props.anchorPosition} panelPaddingSize="none" repositionOnScroll={true} - withTitle={this.props.anchorPosition.includes('down')} ownFocus > {element} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx index 7f6c23dddb9fc..8a41ea81407e4 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiButtonToggle, + EuiButton, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { FilterAggConfigRange } from '../types'; @@ -60,18 +60,17 @@ export const FilterRangeForm: FilterAggConfigRange['aggTypeConfig']['FilterAggFo onChange={(e) => { updateConfig({ from: e.target.value === '' ? undefined : Number(e.target.value) }); }} - // @ts-ignore step="any" prepend={ - '} onChange={(e: any) => { updateConfig({ includeFrom: e.target.checked }); }} - isSelected={includeFrom} - isEmpty={!includeFrom} - /> + fill={includeFrom} + > + {includeFrom ? '≥' : '>'} + } /> @@ -91,18 +90,17 @@ export const FilterRangeForm: FilterAggConfigRange['aggTypeConfig']['FilterAggFo onChange={(e) => { updateConfig({ to: e.target.value === '' ? undefined : Number(e.target.value) }); }} - // @ts-ignore step="any" append={ - { - updateConfig({ includeTo: e.target.checked }); + onClick={() => { + updateConfig({ includeTo: !includeTo }); }} - isSelected={includeTo} - isEmpty={!includeTo} - /> + fill={includeTo} + > + {includeTo ? '≤' : '<'}s + } /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cd090b0c264ef..7386903c311d2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -529,8 +529,6 @@ "core.ui.chrome.headerGlobalNav.helpMenuOpenGitHubIssueTitle": "GitHubで問題を開く", "core.ui.chrome.headerGlobalNav.helpMenuTitle": "ヘルプ", "core.ui.chrome.headerGlobalNav.helpMenuVersion": "v {version}", - "core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近のアイテム", - "core.ui.chrome.sideGlobalNav.viewRecentItemsLabel": "最近閲覧", "core.ui.EmptyRecentlyViewed": "最近閲覧したアイテムはありません", "core.ui.enterpriseSearchNavList.label": "エンタープライズサーチ", "core.ui.errorUrlOverflow.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", @@ -559,7 +557,6 @@ "core.ui.primaryNavSection.undockAriaLabel": "プライマリナビゲーションリンクの固定を解除する", "core.ui.primaryNavSection.undockLabel": "ナビゲーションの固定を解除する", "core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel}、タイプ: {pageType}", - "core.ui.recentLinks.screenReaderLabel": "最近閲覧したリンク、ナビゲーション", "core.ui.recentlyViewed": "最近閲覧", "core.ui.recentlyViewedAriaLabel": "最近閲覧したリンク", "core.ui.securityNavList.label": "セキュリティ", @@ -3671,7 +3668,6 @@ "visDefaultEditor.editorConfig.dateHistogram.customInterval.helpText": "構成間隔の倍数でなければなりません: {interval}", "visDefaultEditor.editorConfig.histogram.interval.helpText": "構成間隔の倍数でなければなりません: {interval}", "visDefaultEditor.metrics.wrongLastBucketTypeErrorMessage": "「{type}」メトリック集約を使用する場合、最後のバケット集約は「Date Histogram」または「Histogram」でなければなりません。", - "visDefaultEditor.sidebar.autoApplyChangesAriaLabel": "エディターの変更を自動適用します", "visDefaultEditor.sidebar.autoApplyChangesOffLabel": "自動適用がオフです", "visDefaultEditor.sidebar.autoApplyChangesOnLabel": "自動適用がオンです", "visDefaultEditor.sidebar.autoApplyChangesTooltip": "変更されるごとにビジュアライゼーションを自動的に更新します。", @@ -5105,10 +5101,10 @@ "xpack.apm.serviceOverview.mlNudgeMessage.content": "ML異常検知との統合により、サービスの正常性ステータスを確認できます", "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "メッセージを消去", "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "詳細", - "xpack.apm.serviceOverview.mlNudgeMessage.title": "異常検知を有効にしてサービスのヘルスを確認", - "xpack.apm.serviceOverview.toastText": "現在 Elastic Stack 7.0+ を実行中で、以前のバージョン 6.x からの互換性のないデータを検知しました。このデータを APM で表示するには、移行が必要です。詳細: ", - "xpack.apm.serviceOverview.toastTitle": "選択された時間範囲内にレガシーデータが検知されました。", - "xpack.apm.serviceOverview.upgradeAssistantLink": "アップグレードアシスタント", + "xpack.apm.serviceInventory.mlNudgeMessageTitle": "異常検知を有効にしてサービスのヘルスを確認", + "xpack.apm.serviceInventory.toastText": "現在 Elastic Stack 7.0+ を実行中で、以前のバージョン 6.x からの互換性のないデータを検知しました。このデータを APM で表示するには、移行が必要です。詳細: ", + "xpack.apm.serviceInventory.toastTitle": "選択された時間範囲内にレガシーデータが検知されました。", + "xpack.apm.serviceInventory.upgradeAssistantLinkText": "アップグレードアシスタント", "xpack.apm.servicesTable.7xOldDataMessage": "また、移行が必要な古いデータがある可能性もあります。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "バージョン7.xより前からのアップグレードですか?また、\n APMサーバーインスタンスを7.0以降にアップグレードしていることも確認してください。", "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均応答時間", @@ -5208,7 +5204,7 @@ "xpack.apm.settings.customizeUI.customLink.table.url": "URL", "xpack.apm.settings.indices": "インデックス", "xpack.apm.settings.pageTitle": "設定", - "xpack.apm.settings.returnToOverviewLinkLabel": "概要に戻る", + "xpack.apm.settings.returnLinkLabel": "概要に戻る", "xpack.apm.settingsLinkLabel": "設定", "xpack.apm.setupInstructionsButtonLabel": "セットアップの手順", "xpack.apm.stacktraceTab.causedByFramesToogleButtonLabel": "作成元", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 18403d85d5ff7..3dfa1cbbca434 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -529,8 +529,6 @@ "core.ui.chrome.headerGlobalNav.helpMenuOpenGitHubIssueTitle": "在 GitHub 中提出问题", "core.ui.chrome.headerGlobalNav.helpMenuTitle": "帮助", "core.ui.chrome.headerGlobalNav.helpMenuVersion": "v {version}", - "core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle": "最近项", - "core.ui.chrome.sideGlobalNav.viewRecentItemsLabel": "最近查看", "core.ui.EmptyRecentlyViewed": "没有最近查看的项目", "core.ui.enterpriseSearchNavList.label": "企业搜索", "core.ui.errorUrlOverflow.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", @@ -559,7 +557,6 @@ "core.ui.primaryNavSection.undockAriaLabel": "取消停靠主导航", "core.ui.primaryNavSection.undockLabel": "取消停靠导航", "core.ui.recentLinks.linkItem.screenReaderLabel": "{recentlyAccessedItemLinklabel},类型:{pageType}", - "core.ui.recentLinks.screenReaderLabel": "最近查看的链接, 导航", "core.ui.recentlyViewed": "最近查看", "core.ui.recentlyViewedAriaLabel": "最近查看的链接", "core.ui.securityNavList.label": "安全", @@ -3672,7 +3669,6 @@ "visDefaultEditor.editorConfig.dateHistogram.customInterval.helpText": "必须是配置时间间隔的倍数:{interval}", "visDefaultEditor.editorConfig.histogram.interval.helpText": "必须是配置时间间隔的倍数:{interval}", "visDefaultEditor.metrics.wrongLastBucketTypeErrorMessage": "使用“{type}”指标聚合时,上一存储桶聚合必须是“Date Histogram”或“Histogram”。", - "visDefaultEditor.sidebar.autoApplyChangesAriaLabel": "自动应用编辑器更改", "visDefaultEditor.sidebar.autoApplyChangesOffLabel": "自动应用关闭", "visDefaultEditor.sidebar.autoApplyChangesOnLabel": "自动应用开启", "visDefaultEditor.sidebar.autoApplyChangesTooltip": "每次更改时自动更新可视化。", @@ -5109,10 +5105,10 @@ "xpack.apm.serviceOverview.mlNudgeMessage.content": "我们集成了 ML 异常检测,让您可以查看服务的运行状况", "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "关闭消息", "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "了解详情", - "xpack.apm.serviceOverview.mlNudgeMessage.title": "启用异常检测可查看服务的运行状况", - "xpack.apm.serviceOverview.toastText": "您正在运行 Elastic Stack 7.0+,我们检测到来自以前 6.x 版本的不兼容数据。如果想在 APM 中查看,您应迁移这些数据。在以下位置查看更多: ", - "xpack.apm.serviceOverview.toastTitle": "在选定时间范围中检测到旧数据", - "xpack.apm.serviceOverview.upgradeAssistantLink": "升级助手", + "xpack.apm.serviceInventory.mlNudgeMessageTitle": "启用异常检测可查看服务的运行状况", + "xpack.apm.serviceInventory.toastText": "您正在运行 Elastic Stack 7.0+,我们检测到来自以前 6.x 版本的不兼容数据。如果想在 APM 中查看,您应迁移这些数据。在以下位置查看更多: ", + "xpack.apm.serviceInventory.toastTitle": "在选定时间范围中检测到旧数据", + "xpack.apm.serviceInventory.upgradeAssistantLinkText": "升级助手", "xpack.apm.servicesTable.7xOldDataMessage": "可能还有需要迁移的旧数据。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "从 7.x 之前的版本升级?另外,确保您已将\n APM Server 实例升级到至少 7.0。", "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均响应时间", @@ -5212,7 +5208,7 @@ "xpack.apm.settings.customizeUI.customLink.table.url": "URL", "xpack.apm.settings.indices": "索引", "xpack.apm.settings.pageTitle": "设置", - "xpack.apm.settings.returnToOverviewLinkLabel": "返回到概览", + "xpack.apm.settings.returnLinkLabel": "返回到概览", "xpack.apm.settingsLinkLabel": "璁剧疆", "xpack.apm.setupInstructionsButtonLabel": "设置说明", "xpack.apm.stacktraceTab.causedByFramesToogleButtonLabel": "原因", diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index a4446e0a75120..72e1f0be5f7f4 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["home", "alerts", "stackAlerts"], + "optionalPlugins": ["alerts", "stackAlerts", "features", "home"], "requiredPlugins": ["management", "charts", "data", "kibanaReact"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index c53dc0c105084..fc48a8e977c7d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -17,9 +17,9 @@ import { ScopedHistory, } from 'kibana/public'; import { Section, routeToAlertDetails } from './constants'; +import { KibanaFeature } from '../../../features/common'; import { AppContextProvider } from './app_context'; -import { ActionTypeModel, AlertTypeModel } from '../types'; -import { TypeRegistry } from './type_registry'; +import { ActionTypeRegistryContract, AlertTypeRegistryContract } from '../types'; import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerts/public'; @@ -42,9 +42,10 @@ export interface AppDeps { uiSettings: IUiSettingsClient; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; capabilities: ApplicationStart['capabilities']; - actionTypeRegistry: TypeRegistry; - alertTypeRegistry: TypeRegistry; + actionTypeRegistry: ActionTypeRegistryContract; + alertTypeRegistry: AlertTypeRegistryContract; history: ScopedHistory; + kibanaFeatures: KibanaFeature[]; } export const App = (appDeps: AppDeps) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx index b33d5e16c6eb9..ccc2ddd9c01ca 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx @@ -188,7 +188,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent - + setPopoverOpen(false)} ownFocus - withTitle anchorPosition="downLeft" zIndex={8000} display="block" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index cc00c244ecd02..7c42c43dc79a2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -314,7 +314,6 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent; + actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: ToastsSetup; capabilities: ApplicationStart['capabilities']; reloadConnectors?: () => Promise; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx index b4cf13538d64d..a4293f94268ba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx @@ -18,14 +18,13 @@ import { DataPublicPluginStartUi, IndexPatternsContract, } from 'src/plugins/data/public'; -import { TypeRegistry } from '../type_registry'; -import { AlertTypeModel, ActionTypeModel } from '../../types'; +import { AlertTypeRegistryContract, ActionTypeRegistryContract } from '../../types'; export interface AlertsContextValue> { reloadAlerts?: () => Promise; http: HttpSetup; - alertTypeRegistry: TypeRegistry; - actionTypeRegistry: TypeRegistry; + alertTypeRegistry: AlertTypeRegistryContract; + actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: ToastsStart; uiSettings?: IUiSettingsClient; charts?: ChartsPluginSetup; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx new file mode 100644 index 0000000000000..d19dc3d303479 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { RouteComponentProps, Router } from 'react-router-dom'; +import { createMemoryHistory, createLocation } from 'history'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import TriggersActionsUIHome, { MatchParams } from './home'; +import { AppContextProvider } from './app_context'; +import { getMockedAppDependencies } from './test_utils'; + +describe('home', () => { + it('renders the documentation link', async () => { + const deps = await getMockedAppDependencies(); + + const props: RouteComponentProps = { + history: createMemoryHistory(), + location: createLocation('/'), + match: { + isExact: true, + path: `/alerts`, + url: '', + params: { + section: 'alerts', + }, + }, + }; + const wrapper = mountWithIntl( + + + + + + ); + const documentationLink = wrapper.find('[data-test-subj="documentationLink"]'); + expect(documentationLink.exists()).toBeTruthy(); + expect(documentationLink.first().prop('href')).toEqual( + 'https://www.elastic.co/guide/en/kibana/mocked-test-branch/managing-alerts-and-actions.html' + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 482b38ffc0d68..450f33d4f7e89 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -10,13 +10,14 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, EuiSpacer, EuiTab, EuiTabs, EuiTitle, EuiText, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { Section, routeToConnectors, routeToAlerts } from './constants'; @@ -30,7 +31,7 @@ import { AlertsList } from './sections/alerts_list/components/alerts_list'; import { HealthCheck } from './components/health_check'; import { HealthContextProvider } from './context/health_context'; -interface MatchParams { +export interface MatchParams { section: Section; } @@ -80,27 +81,40 @@ export const TriggersActionsUIHome: React.FunctionComponent - - - + + +

-
- - -

+ + + -

-
-
-
+ +
+
+ + + +

+ +

+
{tabs.map((tab) => ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index 60ec8004983a3..cf83062b5781e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -22,7 +22,7 @@ describe('action_connector_form', () => { ] = await mocks.getStartServices(); deps = { http: mocks.http, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, capabilities, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index f91bd7382b61c..3a1f9872a96a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -24,10 +24,9 @@ import { ReducerAction } from './connector_reducer'; import { ActionConnector, IErrorObject, - ActionTypeModel, + ActionTypeRegistryContract, UserConfiguredActionConnector, } from '../../../types'; -import { TypeRegistry } from '../../type_registry'; import { hasSaveActionsCapability } from '../../lib/capabilities'; export function validateBaseProperties(actionObject: ActionConnector) { @@ -61,7 +60,7 @@ interface ActionConnectorProps< }; errors: IErrorObject; http: HttpSetup; - actionTypeRegistry: TypeRegistry; + actionTypeRegistry: ActionTypeRegistryContract; docLinks: DocLinksStart; capabilities: ApplicationStart['capabilities']; consumer?: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 3e229c6a2333d..7c718e8248e41 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -178,7 +178,7 @@ describe('action_form', () => { }, }, setHasActionsWithBrokenConnector: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; actionTypeRegistry.list.mockReturnValue([ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 61cf3f2d37925..51d3b0074ca54 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -34,6 +34,7 @@ import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/act import { IErrorObject, ActionTypeModel, + ActionTypeRegistryContract, AlertAction, ActionTypeIndex, ActionConnector, @@ -42,7 +43,6 @@ import { } from '../../../types'; import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from './connector_add_modal'; -import { TypeRegistry } from '../../type_registry'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; @@ -55,7 +55,7 @@ interface ActionAccordionFormProps { setAlertProperty: (actions: AlertAction[]) => void; setActionParamsProperty: (key: string, value: any, index: number) => void; http: HttpSetup; - actionTypeRegistry: TypeRegistry; + actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: ToastsSetup; docLinks: DocLinksStart; actionTypes?: ActionType[]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx index a5e9cdc65cfa6..2fe068536f4a1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -33,7 +33,7 @@ describe('connector_add_flyout', () => { show: true, }, }, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index 0863465833c0b..b32a8ed4161d6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -34,7 +34,7 @@ describe('connector_add_flyout', () => { show: true, }, }, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index 3d621367fc40a..cba9eea3cf3f7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -32,7 +32,7 @@ describe('connector_add_modal', () => { delete: true, }, }, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index d7ca91218d4dd..13ec8395aa557 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -19,12 +19,16 @@ import { EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { HttpSetup, ToastsApi, ApplicationStart, DocLinksStart } from 'kibana/public'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; -import { ActionType, ActionConnector, IErrorObject, ActionTypeModel } from '../../../types'; import { connectorReducer } from './connector_reducer'; import { createActionConnector } from '../../lib/action_connector_api'; -import { TypeRegistry } from '../../type_registry'; import './connector_add_modal.scss'; import { hasSaveActionsCapability } from '../../lib/capabilities'; +import { + ActionType, + ActionConnector, + IErrorObject, + ActionTypeRegistryContract, +} from '../../../types'; interface ConnectorAddModalProps { actionType: ActionType; @@ -32,7 +36,7 @@ interface ConnectorAddModalProps { setAddModalVisibility: React.Dispatch>; postSaveEventHandler?: (savedAction: ActionConnector) => void; http: HttpSetup; - actionTypeRegistry: TypeRegistry; + actionTypeRegistry: ActionTypeRegistryContract; toastNotifications: Pick< ToastsApi, 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index 0c2f4df0ca52b..ac379e279f7f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -35,7 +35,7 @@ describe('connector_edit_flyout', () => { show: true, }, }, - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, alertTypeRegistry: {} as any, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index c96e62df71ce4..e946e881def10 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -15,6 +15,7 @@ import { AppContextProvider } from '../../../app_context'; import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; import { alertingPluginMock } from '../../../../../../alerts/public/mocks'; +import { featuresPluginMock } from '../../../../../../features/public/mocks'; jest.mock('../../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), @@ -49,6 +50,8 @@ describe('actions_connectors_list component empty', () => { application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); + const deps = { chrome, docLinks, @@ -69,8 +72,9 @@ describe('actions_connectors_list component empty', () => { }, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, + actionTypeRegistry, alertTypeRegistry: {} as any, + kibanaFeatures, }; actionTypeRegistry.has.mockReturnValue(true); @@ -156,6 +160,8 @@ describe('actions_connectors_list component with items', () => { application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); + const deps = { chrome, docLinks, @@ -182,6 +188,7 @@ describe('actions_connectors_list component with items', () => { }, } as any, alertTypeRegistry: {} as any, + kibanaFeatures, }; wrapper = mountWithIntl( @@ -244,6 +251,8 @@ describe('actions_connectors_list component empty with show only capability', () application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); + const deps = { chrome, docLinks, @@ -270,6 +279,7 @@ describe('actions_connectors_list component empty with show only capability', () }, } as any, alertTypeRegistry: {} as any, + kibanaFeatures, }; wrapper = mountWithIntl( @@ -333,6 +343,8 @@ describe('actions_connectors_list with show only capability', () => { application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); + const deps = { chrome, docLinks, @@ -359,6 +371,7 @@ describe('actions_connectors_list with show only capability', () => { }, } as any, alertTypeRegistry: {} as any, + kibanaFeatures, }; wrapper = mountWithIntl( @@ -434,6 +447,8 @@ describe('actions_connectors_list component with disabled items', () => { application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); + const deps = { chrome, docLinks, @@ -460,6 +475,7 @@ describe('actions_connectors_list component with disabled items', () => { }, } as any, alertTypeRegistry: {} as any, + kibanaFeatures, }; wrapper = mountWithIntl( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 8ac80c4ad2880..0ac20626e1044 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -84,8 +84,8 @@ describe('alert_add', () => { uiSettings: mocks.uiSettings, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 24eb7aabb9549..fe86e5da98765 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -36,8 +36,8 @@ describe('alert_edit', () => { toastNotifications: mockedCoreSetup.notifications.toasts, http: mockedCoreSetup.http, uiSettings: mockedCoreSetup.uiSettings, - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, capabilities, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 6091519f5851e..cda791489d7f7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -98,8 +98,8 @@ describe('alert_form', () => { toastNotifications: mocks.notifications.toasts, http: mocks.http, uiSettings: mocks.uiSettings, - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, capabilities, }; @@ -231,8 +231,8 @@ describe('alert_form', () => { toastNotifications: mocks.notifications.toasts, http: mocks.http, uiSettings: mocks.uiSettings, - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, capabilities, }; @@ -332,8 +332,8 @@ describe('alert_form', () => { toastNotifications: mockes.notifications.toasts, http: mockes.http, uiSettings: mockes.uiSettings, - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, }; alertTypeRegistry.list.mockReturnValue([alertType]); alertTypeRegistry.get.mockReturnValue(alertType); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index d2ca0abe566ad..bdc11fd543ee1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -401,7 +401,7 @@ export const AlertForm = ({ 0} error={errors.interval} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 86b9afd9565f8..21dd17b538c63 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -18,6 +18,7 @@ import { chartPluginMock } from '../../../../../../../../src/plugins/charts/publ import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; import { alertingPluginMock } from '../../../../../../alerts/public/mocks'; import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common'; +import { featuresPluginMock } from '../../../../../../features/public/mocks'; jest.mock('../../../lib/action_connector_api', () => ({ loadActionTypes: jest.fn(), @@ -96,6 +97,9 @@ describe('alerts_list component empty', () => { application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); + + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); + const deps = { chrome, docLinks, @@ -109,8 +113,9 @@ describe('alerts_list component empty', () => { capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, + kibanaFeatures, }; wrapper = mountWithIntl( @@ -265,6 +270,7 @@ describe('alerts_list component with items', () => { application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); const deps = { chrome, docLinks, @@ -278,8 +284,9 @@ describe('alerts_list component with items', () => { capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, + kibanaFeatures, }; alertTypeRegistry.has.mockReturnValue(true); @@ -346,6 +353,7 @@ describe('alerts_list component empty with show only capability', () => { application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); const deps = { chrome, docLinks, @@ -365,6 +373,7 @@ describe('alerts_list component empty with show only capability', () => { }, } as any, alertTypeRegistry: {} as any, + kibanaFeatures, }; wrapper = mountWithIntl( @@ -465,6 +474,7 @@ describe('alerts_list with show only capability', () => { application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); const deps = { chrome, docLinks, @@ -478,8 +488,9 @@ describe('alerts_list with show only capability', () => { capabilities, history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, + actionTypeRegistry, + alertTypeRegistry, + kibanaFeatures, }; alertTypeRegistry.has.mockReturnValue(false); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 9eb1149cf3905..3f39c698597ce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -7,6 +7,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { i18n } from '@kbn/i18n'; +import { capitalize, sortBy } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useEffect, useState, Fragment } from 'react'; import { @@ -78,6 +79,7 @@ export const AlertsList: React.FunctionComponent = () => { docLinks, charts, dataPlugin, + kibanaFeatures, } = useAppDependencies(); const canExecuteActions = hasExecuteActionsCapability(capabilities); @@ -334,16 +336,43 @@ export const AlertsList: React.FunctionComponent = () => { (alertType) => alertType.authorizedConsumers[ALERTS_FEATURE_ID]?.all ); + const getProducerFeatureName = (producer: string) => { + return kibanaFeatures?.find((featureItem) => featureItem.id === producer)?.name; + }; + + const groupAlertTypesByProducer = () => { + return authorizedAlertTypes.reduce( + ( + result: Record< + string, + Array<{ + value: string; + name: string; + }> + >, + alertType + ) => { + const producer = alertType.producer; + (result[producer] = result[producer] || []).push({ + value: alertType.id, + name: alertType.name, + }); + return result; + }, + {} + ); + }; + const toolsRight = [ setTypesFilter(types)} - options={authorizedAlertTypes - .map((alertType) => ({ - value: alertType.id, - name: alertType.name, - })) - .sort((a, b) => a.name.localeCompare(b.name))} + options={sortBy(Object.entries(groupAlertTypesByProducer())).map( + ([groupName, alertTypesOptions]) => ({ + groupName: getProducerFeatureName(groupName) ?? capitalize(groupName), + subOptions: alertTypesOptions.sort((a, b) => a.name.localeCompare(b.name)), + }) + )} />, ; }>; onChange?: (selectedTags: string[]) => void; } @@ -52,22 +61,29 @@ export const TypeFilter: React.FunctionComponent = ({ } >
- {options.map((item, index) => ( - { - const isPreviouslyChecked = selectedValues.includes(item.value); - if (isPreviouslyChecked) { - setSelectedValues(selectedValues.filter((val) => val !== item.value)); - } else { - setSelectedValues(selectedValues.concat(item.value)); - } - }} - checked={selectedValues.includes(item.value) ? 'on' : undefined} - data-test-subj={`alertType${item.value}FilterOption`} - > - {item.name} - + {options.map((groupItem, groupIndex) => ( + + +

{groupItem.groupName}

+
+ {groupItem.subOptions.map((item, index) => ( + { + const isPreviouslyChecked = selectedValues.includes(item.value); + if (isPreviouslyChecked) { + setSelectedValues(selectedValues.filter((val) => val !== item.value)); + } else { + setSelectedValues(selectedValues.concat(item.value)); + } + }} + checked={selectedValues.includes(item.value) ? 'on' : undefined} + data-test-subj={`alertType${item.value}FilterOption`} + > + {item.name} + + ))} +
))}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts new file mode 100644 index 0000000000000..b5ab53d868cf1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { featuresPluginMock } from '../../../../features/public/mocks'; +import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { alertingPluginMock } from '../../../../alerts/public/mocks'; +import { actionTypeRegistryMock } from '../action_type_registry.mock'; +import { alertTypeRegistryMock } from '../alert_type_registry.mock'; +import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; + +export async function getMockedAppDependencies() { + const coreSetupMock = coreMock.createSetup(); + const actionTypeRegistry = actionTypeRegistryMock.create(); + const alertTypeRegistry = alertTypeRegistryMock.create(); + const [ + { + chrome, + docLinks, + application: { capabilities, navigateToApp }, + }, + ] = await coreSetupMock.getStartServices(); + const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); + + return { + chrome, + docLinks, + dataPlugin: dataPluginMock.createStartContract(), + charts: chartPluginMock.createStartContract(), + alerting: alertingPluginMock.createStartContract(), + toastNotifications: coreSetupMock.notifications.toasts, + http: coreSetupMock.http, + uiSettings: coreSetupMock.uiSettings, + navigateToApp, + capabilities, + history: scopedHistoryMock.create(), + setBreadcrumbs: jest.fn(), + actionTypeRegistry, + alertTypeRegistry, + kibanaFeatures, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx index 08339b509d5fd..388f87cbf752b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx @@ -83,7 +83,6 @@ export const ForLastExpression = ({ }} ownFocus display={display === 'fullWidth' ? 'block' : 'inlineBlock'} - withTitle anchorPosition={popupPosition ?? 'downLeft'} >
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx index 6af103be96e13..785df0981ebe6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx @@ -113,7 +113,6 @@ export const GroupByExpression = ({ setGroupByPopoverOpen(false); }} ownFocus - withTitle display={display === 'fullWidth' ? 'block' : 'inlineBlock'} anchorPosition={popupPosition ?? 'downRight'} > diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx index 9cea1d3812274..e15b9a21570c9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx @@ -101,7 +101,6 @@ export const OfExpression = ({ closePopover={() => { setAggFieldPopoverOpen(false); }} - withTitle display={display === 'fullWidth' ? 'block' : 'inlineBlock'} anchorPosition={popupPosition ?? 'downRight'} zIndex={8000} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index 2b5cec98b16a1..bdf30414b68bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -97,7 +97,6 @@ export const ThresholdExpression = ({ setAlertThresholdPopoverOpen(false); }} ownFocus - withTitle display={display === 'fullWidth' ? 'block' : 'inlineBlock'} anchorPosition={popupPosition ?? 'downLeft'} > diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx index 18197b6f64e43..5696417f241fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx @@ -66,7 +66,6 @@ export const WhenExpression = ({ }} ownFocus display={display === 'fullWidth' ? 'block' : 'inlineBlock'} - withTitle anchorPosition={popupPosition ?? 'downLeft'} >
diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 393ac5bc1b74d..b22be6ef9b2f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -12,6 +12,7 @@ import { } from 'src/core/public'; import { i18n } from '@kbn/i18n'; +import { FeaturesPluginStart } from '../../features/public'; import { registerBuiltInActionTypes } from './application/components/builtin_action_types'; import { registerBuiltInAlertTypes } from './application/components/builtin_alert_types'; import { ActionTypeModel, AlertTypeModel } from './types'; @@ -52,6 +53,7 @@ interface PluginsStart { charts: ChartsPluginStart; alerts?: AlertingStart; navigateToApp: CoreStart['application']['navigateToApp']; + features: FeaturesPluginStart; } export class Plugin @@ -112,6 +114,7 @@ export class Plugin ]; const { boot } = await import('./application/boot'); + const kibanaFeatures = await pluginsStart.features.getFeatures(); return boot({ dataPlugin: pluginsStart.data, @@ -131,6 +134,7 @@ export class Plugin history: params.history, actionTypeRegistry, alertTypeRegistry, + kibanaFeatures, }); }, }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx index bd0191443d785..3251e85841d86 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx @@ -189,7 +189,6 @@ function AddVariableButton({ closePopover={closePopover} panelPaddingSize="none" anchorPosition="downLeft" - withTitle > = [ + { + id: 'dynamicActions', + getDisplayName: () => + i18n.translate('xpack.uiActionsEnhanced.CustomActions', { + defaultMessage: 'Custom actions', + }), + getIconType: () => 'symlink', + order: 26, + }, +]; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index cdd357f3560b8..cbc381c911c3d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -13,6 +13,7 @@ import { UiActionsServiceEnhancements } from '../services'; import { ActionFactoryDefinition } from './action_factory_definition'; import { SerializedAction, SerializedEvent } from './types'; import { licensingMock } from '../../../licensing/public/mocks'; +import { dynamicActionGrouping } from './dynamic_action_grouping'; const actionFactoryDefinition1: ActionFactoryDefinition = { id: 'ACTION_FACTORY_1', @@ -294,6 +295,27 @@ describe('DynamicActionManager', () => { expect(manager.state.get().events.length).toBe(1); }); + test('adds revived actiosn to "dynamic action" grouping', async () => { + const { manager, uiActions, actions } = setup([]); + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(manager.state.get().events.length).toBe(0); + + await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']); + + const createdAction = actions.values().next().value; + + expect(createdAction.grouping).toBe(dynamicActionGrouping); + }); + test('optimistically adds event to UI state', async () => { const { manager, uiActions } = setup([]); const action: SerializedAction = { diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index b414296690c9e..f096b17f8a78d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -18,6 +18,7 @@ import { } from '../../../../../src/plugins/kibana_utils/common'; import { StartContract } from '../plugin'; import { SerializedAction, SerializedEvent } from './types'; +import { dynamicActionGrouping } from './dynamic_action_grouping'; const compareEvents = ( a: ReadonlyArray<{ eventId: string }>, @@ -93,6 +94,7 @@ export class DynamicActionManager { uiActions.registerAction({ ...actionDefinition, id: actionId, + grouping: dynamicActionGrouping, isCompatible: async (context) => { if (!(await isCompatible(context))) return false; if (!actionDefinition.isCompatible) return true; diff --git a/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap index c540334222418..a3a6d44a0320b 100644 --- a/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap @@ -69,7 +69,7 @@ exports[`UptimeDatePicker component renders properly with mock data 1`] = ` class="euiToolTipAnchor" >
diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx index 064907a633df0..902f497babda8 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx @@ -93,7 +93,6 @@ export const FilterPopover = ({ id={id} isOpen={isOpen || forceOpen} ownFocus={true} - withTitle zIndex={10000} > diff --git a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap index 15389460517de..7bb578494ab44 100644 --- a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap +++ b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap @@ -276,7 +276,7 @@ Array [ class="euiToolTipAnchor" >