diff --git a/.eslintignore b/.eslintignore index fbdd70703f3c4..9de2cc2872960 100644 --- a/.eslintignore +++ b/.eslintignore @@ -33,6 +33,7 @@ target /x-pack/plugins/canvas/shareable_runtime/build /x-pack/plugins/canvas/storybook /x-pack/plugins/monitoring/public/lib/jquery_flot +/x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/** /x-pack/legacy/plugins/infra/common/graphql/types.ts /x-pack/legacy/plugins/infra/public/graphql/types.ts /x-pack/legacy/plugins/infra/server/graphql/types.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md new file mode 100644 index 0000000000000..64805eefbfea1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getCustomNavLink$](./kibana-plugin-core-public.chromestart.getcustomnavlink_.md) + +## ChromeStart.getCustomNavLink$() method + +Get an observable of the current custom nav link + +Signature: + +```typescript +getCustomNavLink$(): Observable | undefined>; +``` +Returns: + +`Observable | undefined>` + 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 b4eadc93fe78d..e983ad50d2afe 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 | +| [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. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | @@ -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 | +| [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 | | [setIsVisible(isVisible)](./kibana-plugin-core-public.chromestart.setisvisible.md) | Set the temporary visibility for the chrome. This does nothing if the chrome is hidden by default and should be used to hide the chrome for things like full-screen modes with an exit button. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md new file mode 100644 index 0000000000000..adfb57f9c5ff2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [setCustomNavLink](./kibana-plugin-core-public.chromestart.setcustomnavlink.md) + +## ChromeStart.setCustomNavLink() method + +Override the current set of custom nav link + +Signature: + +```typescript +setCustomNavLink(newCustomNavLink?: Partial): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newCustomNavLink | Partial<ChromeNavLink> | | + +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index e9ed5b830b691..32221a320d2a1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -22,7 +22,6 @@ export interface CoreSetupStartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | | [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | -| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | | [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | | [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md deleted file mode 100644 index 77c9e867ef8ea..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [metrics](./kibana-plugin-core-server.coresetup.metrics.md) - -## CoreSetup.metrics property - -[MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) - -Signature: - -```typescript -metrics: MetricsServiceSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index 6a6bacf1eef40..acd23f0f47386 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -19,6 +19,7 @@ export interface CoreStart | [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | | [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | +| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart | | | [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | | [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md new file mode 100644 index 0000000000000..a51c2f842c346 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStart](./kibana-plugin-core-server.corestart.md) > [metrics](./kibana-plugin-core-server.corestart.metrics.md) + +## CoreStart.metrics property + + +Signature: + +```typescript +metrics: MetricsServiceStart; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 1a03ac5ee3d1a..74422c82fc9ea 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -112,7 +112,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LoggerFactory](./kibana-plugin-core-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. | | [LogMeta](./kibana-plugin-core-server.logmeta.md) | Contextual metadata | -| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | +| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | | [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) | | | [OnPostAuthToolkit](./kibana-plugin-core-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | @@ -150,7 +150,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsBulkUpdateResponse](./kibana-plugin-core-server.savedobjectsbulkupdateresponse.md) | | | [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | -| [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | +| [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation.Note: this type intentially doesn't include a type definition for defining the dynamic mapping parameter. Saved Object fields should always inherit the dynamic: 'strict' paramater. If you are unsure of the shape of your data use type: 'object', enabled: false instead. | | [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | | | [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md deleted file mode 100644 index 61107fbf20ad9..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) > [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) - -## MetricsServiceSetup.getOpsMetrics$ property - -Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, based on the `opts.interval` configuration property. - -Signature: - -```typescript -getOpsMetrics$: () => Observable; -``` - -## Example - - -```ts -core.metrics.getOpsMetrics$().subscribe(metrics => { - // do something with the metrics -}) - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md index 00045aeac74b4..0bec919797b6f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md @@ -4,17 +4,8 @@ ## MetricsServiceSetup interface -APIs to retrieves metrics gathered and exposed by the core platform. - Signature: ```typescript export interface MetricsServiceSetup ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) | () => Observable<OpsMetrics> | Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's start phase, and a new value every fixed interval of time, based on the opts.interval configuration property. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md deleted file mode 100644 index e63e543e68d51..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) > [dynamic](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md) - -## SavedObjectsComplexFieldMapping.dynamic property - -Signature: - -```typescript -dynamic?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md index 60e62212609d9..a7d13b0015e3f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md @@ -6,6 +6,8 @@ See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. +Note: this type intentially doesn't include a type definition for defining the `dynamic` mapping parameter. Saved Object fields should always inherit the `dynamic: 'strict'` paramater. If you are unsure of the shape of your data use `type: 'object', enabled: false` instead. + Signature: ```typescript @@ -16,7 +18,6 @@ export interface SavedObjectsComplexFieldMapping | Property | Type | Description | | --- | --- | --- | -| [dynamic](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md) | string | | | [properties](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.properties.md) | SavedObjectsMappingProperties | | | [type](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.type.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md new file mode 100644 index 0000000000000..f1916e89c2c98 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) + +## isFilter variable + +Signature: + +```typescript +isFilter: (x: unknown) => x is Filter +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md new file mode 100644 index 0000000000000..558da72cc26bb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) + +## isFilters variable + +Signature: + +```typescript +isFilters: (x: unknown) => x is Filter[] +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md new file mode 100644 index 0000000000000..0884566333aa8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isQuery](./kibana-plugin-plugins-data-public.isquery.md) + +## isQuery variable + +Signature: + +```typescript +isQuery: (x: unknown) => x is Query +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md new file mode 100644 index 0000000000000..e9420493c82fb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) + +## isTimeRange variable + +Signature: + +```typescript +isTimeRange: (x: unknown) => x is TimeRange +``` 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 f62479f02926e..feeb686a1f5ed 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 @@ -110,6 +110,10 @@ | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | | [injectSearchSourceReferences](./kibana-plugin-plugins-data-public.injectsearchsourcereferences.md) | | +| [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) | | +| [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) | | +| [isQuery](./kibana-plugin-plugins-data-public.isquery.md) | | +| [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) | | | [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | | | [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | | | [search](./kibana-plugin-plugins-data-public.search.md) | | diff --git a/docs/development/plugins/kibana_utils/common/state_containers/index.md b/docs/development/plugins/kibana_utils/common/state_containers/index.md new file mode 100644 index 0000000000000..b4e1071ceb732 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/index.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) + +## API Reference + +## Packages + +| Package | Description | +| --- | --- | +| [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) | State containers are Redux-store-like objects meant to help you manage state in your services or apps. Refer to [guides and examples](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers) for more info | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md new file mode 100644 index 0000000000000..92893afc02bef --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseState](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md) + +## BaseState type + +Base [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) state shape + +Signature: + +```typescript +export declare type BaseState = object; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md new file mode 100644 index 0000000000000..b939954d92aa6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) > [get](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md) + +## BaseStateContainer.get property + +Retrieves current state from the container + +Signature: + +```typescript +get: () => State; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md new file mode 100644 index 0000000000000..66c25c87f5e37 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) + +## BaseStateContainer interface + +Base state container shape without transitions or selectors + +Signature: + +```typescript +export interface BaseStateContainer +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [get](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md) | () => State | Retrieves current state from the container | +| [set](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md) | (state: State) => void | Sets state into container | +| [state$](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md) | Observable<State> | of state | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md new file mode 100644 index 0000000000000..ed4ff365adfb3 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) > [set](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md) + +## BaseStateContainer.set property + +Sets state into container + +Signature: + +```typescript +set: (state: State) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md new file mode 100644 index 0000000000000..35838fa53d539 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) > [state$](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md) + +## BaseStateContainer.state$ property + + of state + +Signature: + +```typescript +state$: Observable; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md new file mode 100644 index 0000000000000..12af33756fb19 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Comparator](./kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md) + +## Comparator type + +Used to compare state. see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) + +Signature: + +```typescript +export declare type Comparator = (previous: Result, current: Result) => boolean; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md new file mode 100644 index 0000000000000..e05f1fb392fe6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) + +## Connect type + +Similar to `connect` from react-redux, allows to map state from state container to component's props + +Signature: + +```typescript +export declare type Connect = (mapStateToProp: MapStateToProps>) => (component: ComponentType) => FC>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md new file mode 100644 index 0000000000000..cc43b59676dc1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md) + +## createStateContainer() function + +Creates a state container without transitions and without selectors. + +Signature: + +```typescript +export declare function createStateContainer(defaultState: State): ReduxLikeStateContainer; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| defaultState | State | initial state | + +Returns: + +`ReduxLikeStateContainer` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md new file mode 100644 index 0000000000000..794bf63588312 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md) + +## createStateContainer() function + +Creates a state container with transitions, but without selectors + +Signature: + +```typescript +export declare function createStateContainer(defaultState: State, pureTransitions: PureTransitions): ReduxLikeStateContainer; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| defaultState | State | initial state | +| pureTransitions | PureTransitions | state transitions configuration object. Map of . | + +Returns: + +`ReduxLikeStateContainer` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md new file mode 100644 index 0000000000000..1946baae202f1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md) + +## createStateContainer() function + +Creates a state container with transitions and selectors + +Signature: + +```typescript +export declare function createStateContainer(defaultState: State, pureTransitions: PureTransitions, pureSelectors: PureSelectors, options?: CreateStateContainerOptions): ReduxLikeStateContainer; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| defaultState | State | initial state | +| pureTransitions | PureTransitions | state transitions configuration object. Map of . | +| pureSelectors | PureSelectors | state selectors configuration object. Map of . | +| options | CreateStateContainerOptions | state container options [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) | + +Returns: + +`ReduxLikeStateContainer` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md new file mode 100644 index 0000000000000..4f772c7c54d08 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) > [freeze](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md) + +## CreateStateContainerOptions.freeze property + +Function to use when freezing state. Supply identity function. If not provided, default deepFreeze is use. + +Signature: + +```typescript +freeze?: (state: T) => T; +``` + +## Example + +If you expect that your state will be mutated externally an you cannot prevent that + +```ts +{ + freeze: state => state, +} + +``` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md new file mode 100644 index 0000000000000..d328d306e93e1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) + +## CreateStateContainerOptions interface + +State container options + +Signature: + +```typescript +export interface CreateStateContainerOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [freeze](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md) | <T>(state: T) => T | Function to use when freezing state. Supply identity function. If not provided, default deepFreeze is use. | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md new file mode 100644 index 0000000000000..a6076490c2746 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainerReactHelpers](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md) + +## createStateContainerReactHelpers variable + +Creates helpers for using [State Containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) with react Refer to [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_containers/react.md) for details + +Signature: + +```typescript +createStateContainerReactHelpers: >() => { + Provider: React.Provider; + Consumer: React.Consumer; + context: React.Context; + useContainer: () => Container; + useState: () => UnboxState; + useTransitions: () => Container["transitions"]; + useSelector: (selector: (state: UnboxState) => Result, comparator?: Comparator) => Result; + connect: Connect>; +} +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md new file mode 100644 index 0000000000000..d4057a549bb0d --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md) + +## Dispatch type + +Redux like dispatch + +Signature: + +```typescript +export declare type Dispatch = (action: T) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md new file mode 100644 index 0000000000000..5e4e86ad82d53 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [EnsurePureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md) + +## EnsurePureSelector type + + +Signature: + +```typescript +export declare type EnsurePureSelector = Ensure>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md new file mode 100644 index 0000000000000..0e621e989346b --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [EnsurePureTransition](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md) + +## EnsurePureTransition type + + +Signature: + +```typescript +export declare type EnsurePureTransition = Ensure>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md new file mode 100644 index 0000000000000..8e6a49ac72742 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [MapStateToProps](./kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md) + +## MapStateToProps type + +State container state to component props mapper. See [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) + +Signature: + +```typescript +export declare type MapStateToProps = (state: State) => StateProps; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md new file mode 100644 index 0000000000000..e74ff2c6885be --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md @@ -0,0 +1,52 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) + +## kibana-plugin-plugins-kibana\_utils-common-state\_containers package + +State containers are Redux-store-like objects meant to help you manage state in your services or apps. Refer to [guides and examples](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers) for more info + +## Functions + +| Function | Description | +| --- | --- | +| [createStateContainer(defaultState)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md) | Creates a state container without transitions and without selectors. | +| [createStateContainer(defaultState, pureTransitions)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md) | Creates a state container with transitions, but without selectors | +| [createStateContainer(defaultState, pureTransitions, pureSelectors, options)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md) | Creates a state container with transitions and selectors | + +## Interfaces + +| Interface | Description | +| --- | --- | +| [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) | Base state container shape without transitions or selectors | +| [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) | State container options | +| [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) | Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) Allows to use state container with redux libraries | +| [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) | Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) | + +## Variables + +| Variable | Description | +| --- | --- | +| [createStateContainerReactHelpers](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md) | Creates helpers for using [State Containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) with react Refer to [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_containers/react.md) for details | +| [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) | React hook to apply selector to state container to extract only needed information. Will re-render your component only when the section changes. | +| [useContainerState](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md) | React hooks that returns the latest state of a [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md). | + +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [BaseState](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md) | Base [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) state shape | +| [Comparator](./kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md) | Used to compare state. see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) | +| [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) | Similar to connect from react-redux, allows to map state from state container to component's props | +| [Dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md) | Redux like dispatch | +| [EnsurePureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md) | | +| [EnsurePureTransition](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md) | | +| [MapStateToProps](./kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md) | State container state to component props mapper. See [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) | +| [Middleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md) | Redux like Middleware | +| [PureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md) | | +| [PureSelectorsToSelectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md) | | +| [PureSelectorToSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md) | | +| [Reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md) | Redux like Reducer | +| [Selector](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) | | +| [UnboxState](./kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md) | Utility type for inferring state shape from [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md new file mode 100644 index 0000000000000..574b83306dc95 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Middleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md) + +## Middleware type + +Redux like Middleware + +Signature: + +```typescript +export declare type Middleware = (store: Pick, 'getState' | 'dispatch'>) => (next: (action: TransitionDescription) => TransitionDescription | any) => Dispatch; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md new file mode 100644 index 0000000000000..6ac07cba446f5 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [PureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md) + +## PureSelector type + + +Signature: + +```typescript +export declare type PureSelector = (state: State) => Selector; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md new file mode 100644 index 0000000000000..82a91f7c87e17 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [PureSelectorsToSelectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md) + +## PureSelectorsToSelectors type + + +Signature: + +```typescript +export declare type PureSelectorsToSelectors = { + [K in keyof T]: PureSelectorToSelector>; +}; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md new file mode 100644 index 0000000000000..5c12afd1cd971 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [PureSelectorToSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md) + +## PureSelectorToSelector type + + +Signature: + +```typescript +export declare type PureSelectorToSelector> = ReturnType>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md new file mode 100644 index 0000000000000..519e6ce7d7cfb --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md) + +## Reducer type + +Redux like Reducer + +Signature: + +```typescript +export declare type Reducer = (state: State, action: TransitionDescription) => State; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md new file mode 100644 index 0000000000000..e90da05e30d87 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [addMiddleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md) + +## ReduxLikeStateContainer.addMiddleware property + +Signature: + +```typescript +addMiddleware: (middleware: Middleware) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md new file mode 100644 index 0000000000000..7a9755ee3b65c --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md) + +## ReduxLikeStateContainer.dispatch property + +Signature: + +```typescript +dispatch: (action: TransitionDescription) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md new file mode 100644 index 0000000000000..86e1c6dd34cd6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [getState](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md) + +## ReduxLikeStateContainer.getState property + +Signature: + +```typescript +getState: () => State; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md new file mode 100644 index 0000000000000..0e08119c1eae4 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) + +## ReduxLikeStateContainer interface + +Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) Allows to use state container with redux libraries + +Signature: + +```typescript +export interface ReduxLikeStateContainer extends StateContainer +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [addMiddleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md) | (middleware: Middleware<State>) => void | | +| [dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md) | (action: TransitionDescription) => void | | +| [getState](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md) | () => State | | +| [reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md) | Reducer<State> | | +| [replaceReducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md) | (nextReducer: Reducer<State>) => void | | +| [subscribe](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md) | (listener: (state: State) => void) => () => void | | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md new file mode 100644 index 0000000000000..49eabf19340f2 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md) + +## ReduxLikeStateContainer.reducer property + +Signature: + +```typescript +reducer: Reducer; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md new file mode 100644 index 0000000000000..2582d31d9adc4 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [replaceReducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md) + +## ReduxLikeStateContainer.replaceReducer property + +Signature: + +```typescript +replaceReducer: (nextReducer: Reducer) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md new file mode 100644 index 0000000000000..15139a7bd9f3e --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [subscribe](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md) + +## ReduxLikeStateContainer.subscribe property + +Signature: + +```typescript +subscribe: (listener: (state: State) => void) => () => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md new file mode 100644 index 0000000000000..5c143551d130b --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Selector](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) + +## Selector type + + +Signature: + +```typescript +export declare type Selector = (...args: Args) => Result; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md new file mode 100644 index 0000000000000..23ec1c8e5be01 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) + +## StateContainer interface + +Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) + +Signature: + +```typescript +export interface StateContainer extends BaseStateContainer +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md) | Readonly<PureSelectorsToSelectors<PureSelectors>> | | +| [transitions](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md) | Readonly<PureTransitionsToTransitions<PureTransitions>> | | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md new file mode 100644 index 0000000000000..2afac07b59e39 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) > [selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md) + +## StateContainer.selectors property + +Signature: + +```typescript +selectors: Readonly>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md new file mode 100644 index 0000000000000..4712d3287beef --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) > [transitions](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md) + +## StateContainer.transitions property + +Signature: + +```typescript +transitions: Readonly>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md new file mode 100644 index 0000000000000..d4f99841456d7 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [UnboxState](./kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md) + +## UnboxState type + +Utility type for inferring state shape from [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) + +Signature: + +```typescript +export declare type UnboxState> = Container extends StateContainer ? T : never; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md new file mode 100644 index 0000000000000..fe5f30a9c8472 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) + +## useContainerSelector variable + +React hook to apply selector to state container to extract only needed information. Will re-render your component only when the section changes. + +Signature: + +```typescript +useContainerSelector: , Result>(container: Container, selector: (state: UnboxState) => Result, comparator?: Comparator) => Result +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md new file mode 100644 index 0000000000000..7cef47c58f9d9 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [useContainerState](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md) + +## useContainerState variable + +React hooks that returns the latest state of a [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md). + +Signature: + +```typescript +useContainerState: >(container: Container) => UnboxState +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/index.md b/docs/development/plugins/kibana_utils/public/state_sync/index.md new file mode 100644 index 0000000000000..4b345d9130bd5 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/index.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) + +## API Reference + +## Packages + +| Package | Description | +| --- | --- | +| [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) | State syncing utilities are a set of helpers for syncing your application state with URL or browser storage.They are designed to work together with state containers (). But state containers are not required.State syncing utilities include:- util which: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. - Two types of storage compatible with syncState: - - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. - - Serializes state and persists it to browser storage.Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md new file mode 100644 index 0000000000000..22f70ce22b574 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [createKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md) + +## createKbnUrlStateStorage variable + +Creates [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) state storage + +Signature: + +```typescript +createKbnUrlStateStorage: ({ useHash, history }?: { + useHash: boolean; + history?: History | undefined; +}) => IKbnUrlStateStorage +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md new file mode 100644 index 0000000000000..dccff93ad1724 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [createSessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md) + +## createSessionStorageStateStorage variable + +Creates [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) + +Signature: + +```typescript +createSessionStorageStateStorage: (storage?: Storage) => ISessionStorageStateStorage +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md new file mode 100644 index 0000000000000..29a511d57d7bd --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) + +## IKbnUrlStateStorage.cancel property + +cancels any pending url updates + +Signature: + +```typescript +cancel: () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md new file mode 100644 index 0000000000000..2b55f2aca70c8 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md) + +## IKbnUrlStateStorage.change$ property + +Signature: + +```typescript +change$: (key: string) => Observable; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md new file mode 100644 index 0000000000000..e0e6aa9be4368 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) + +## IKbnUrlStateStorage.flush property + +synchronously runs any pending url updates returned boolean indicates if change occurred + +Signature: + +```typescript +flush: (opts?: { + replace?: boolean; + }) => boolean; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md new file mode 100644 index 0000000000000..0eb60c21fbbbf --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md) + +## IKbnUrlStateStorage.get property + +Signature: + +```typescript +get: (key: string) => State | null; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md new file mode 100644 index 0000000000000..56cefebd2acfe --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) + +## IKbnUrlStateStorage interface + +KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which: 1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See kibana's advanced option for more context state:storeInSessionStorage 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records. [GUIDE](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) + +Signature: + +```typescript +export interface IKbnUrlStateStorage extends IStateStorage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) | () => void | cancels any pending url updates | +| [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md) | <State = unknown>(key: string) => Observable<State | null> | | +| [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) | (opts?: {
replace?: boolean;
}) => boolean | synchronously runs any pending url updates returned boolean indicates if change occurred | +| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md) | <State = unknown>(key: string) => State | null | | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md) | <State>(key: string, state: State, opts?: {
replace: boolean;
}) => Promise<string | undefined> | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md new file mode 100644 index 0000000000000..2eab44d344414 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md) + +## IKbnUrlStateStorage.set property + +Signature: + +```typescript +set: (key: string, state: State, opts?: { + replace: boolean; + }) => Promise; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md new file mode 100644 index 0000000000000..ca69609936405 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) + +## INullableBaseStateContainer interface + +Extension of with one constraint: set state should handle `null` as incoming state + +Signature: + +```typescript +export interface INullableBaseStateContainer extends BaseStateContainer +``` + +## Remarks + +State container for stateSync() have to accept "null" for example, set() implementation could handle null and fallback to some default state this is required to handle edge case, when state in storage becomes empty and syncing is in progress. state container will be notified about about storage becoming empty with null passed in + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md) | (state: State | null) => void | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md new file mode 100644 index 0000000000000..dd2978f59484a --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md) + +## INullableBaseStateContainer.set property + +Signature: + +```typescript +set: (state: State | null) => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md new file mode 100644 index 0000000000000..83131c77132ce --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) > [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md) + +## ISessionStorageStateStorage.get property + +Signature: + +```typescript +get: (key: string) => State | null; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md new file mode 100644 index 0000000000000..7792bc3932f95 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) + +## ISessionStorageStateStorage interface + +[IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) for storing state in browser [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) + +Signature: + +```typescript +export interface ISessionStorageStateStorage extends IStateStorage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md) | <State = unknown>(key: string) => State | null | | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md) | <State>(key: string, state: State) => void | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md new file mode 100644 index 0000000000000..04b0ab01f0d13 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md) + +## ISessionStorageStateStorage.set property + +Signature: + +```typescript +set: (key: string, state: State) => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md new file mode 100644 index 0000000000000..ce771d52a6e60 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md) + +## IStateStorage.cancel property + +Optional method to cancel any pending activity syncState() will call it, if it is provided by IStateStorage + +Signature: + +```typescript +cancel?: () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md new file mode 100644 index 0000000000000..ed6672a3d83c6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md) + +## IStateStorage.change$ property + +Should notify when the stored state has changed + +Signature: + +```typescript +change$?: (key: string) => Observable; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md new file mode 100644 index 0000000000000..2c0b2ee970cc6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md) + +## IStateStorage.get property + +Should retrieve state from the storage and deserialize it + +Signature: + +```typescript +get: (key: string) => State | null; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md new file mode 100644 index 0000000000000..2c34a185fb7b1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) + +## IStateStorage interface + +Any StateStorage have to implement IStateStorage interface StateStorage is responsible for: \* state serialisation / deserialization \* persisting to and retrieving from storage + +For an example take a look at already implemented [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) and [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) state storages + +Signature: + +```typescript +export interface IStateStorage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md) | () => void | Optional method to cancel any pending activity syncState() will call it, if it is provided by IStateStorage | +| [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md) | <State = unknown>(key: string) => Observable<State | null> | Should notify when the stored state has changed | +| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md) | <State = unknown>(key: string) => State | null | Should retrieve state from the storage and deserialize it | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md) | <State>(key: string, state: State) => any | Take in a state object, should serialise and persist | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md new file mode 100644 index 0000000000000..3f286994ed4af --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md) + +## IStateStorage.set property + +Take in a state object, should serialise and persist + +Signature: + +```typescript +set: (key: string, state: State) => any; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md new file mode 100644 index 0000000000000..f9368de4240ac --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) + +## IStateSyncConfig interface + +Config for setting up state syncing with + +Signature: + +```typescript +export interface IStateSyncConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [stateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md) | INullableBaseStateContainer<State> | State container to keep in sync with storage, have to implement [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) interface We encourage to use as a state container, but it is also possible to implement own custom container for advanced use cases | +| [stateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md) | StateStorage | State storage to use, State storage is responsible for serialising / deserialising and persisting / retrieving stored stateThere are common strategies already implemented: see [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) which replicate what State (AppState, GlobalState) in legacy world did | +| [storageKey](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md) | string | Storage key to use for syncing, e.g. storageKey '\_a' should sync state to ?\_a query param | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md new file mode 100644 index 0000000000000..0098dd5c99aeb --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) > [stateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md) + +## IStateSyncConfig.stateContainer property + +State container to keep in sync with storage, have to implement [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) interface We encourage to use as a state container, but it is also possible to implement own custom container for advanced use cases + +Signature: + +```typescript +stateContainer: INullableBaseStateContainer; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md new file mode 100644 index 0000000000000..ef872ba0ba9b5 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) > [stateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md) + +## IStateSyncConfig.stateStorage property + +State storage to use, State storage is responsible for serialising / deserialising and persisting / retrieving stored state + +There are common strategies already implemented: see [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) which replicate what State (AppState, GlobalState) in legacy world did + +Signature: + +```typescript +stateStorage: StateStorage; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md new file mode 100644 index 0000000000000..d3887c23df1e0 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) > [storageKey](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md) + +## IStateSyncConfig.storageKey property + +Storage key to use for syncing, e.g. storageKey '\_a' should sync state to ?\_a query param + +Signature: + +```typescript +storageKey: string; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md new file mode 100644 index 0000000000000..137db68cd6b48 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) + +## ISyncStateRef interface + + +Signature: + +```typescript +export interface ISyncStateRef +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [start](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md) | StartSyncStateFnType | start state syncing | +| [stop](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md) | StopSyncStateFnType | stop state syncing | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md new file mode 100644 index 0000000000000..d8df808ba215f --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) > [start](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md) + +## ISyncStateRef.start property + +start state syncing + +Signature: + +```typescript +start: StartSyncStateFnType; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md new file mode 100644 index 0000000000000..70356dd9d6c79 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) > [stop](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md) + +## ISyncStateRef.stop property + +stop state syncing + +Signature: + +```typescript +stop: StopSyncStateFnType; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md new file mode 100644 index 0000000000000..2b02c98e0d605 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md @@ -0,0 +1,48 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) + +## kibana-plugin-plugins-kibana\_utils-public-state\_sync package + +State syncing utilities are a set of helpers for syncing your application state with URL or browser storage. + +They are designed to work together with state containers (). But state containers are not required. + +State syncing utilities include: + +- [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) util which: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. - Two types of storage compatible with `syncState`: - [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. - [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) - Serializes state and persists it to browser storage. + +Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples + +## Functions + +| Function | Description | +| --- | --- | +| [syncState({ storageKey, stateStorage, stateContainer, })](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) | Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL) Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples | +| [syncStates(stateSyncConfigs)](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md) | | + +## Interfaces + +| Interface | Description | +| --- | --- | +| [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) | KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which: 1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See kibana's advanced option for more context state:storeInSessionStorage 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records. [GUIDE](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) | +| [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) | Extension of with one constraint: set state should handle null as incoming state | +| [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) | [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) for storing state in browser [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) | +| [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) | Any StateStorage have to implement IStateStorage interface StateStorage is responsible for: \* state serialisation / deserialization \* persisting to and retrieving from storageFor an example take a look at already implemented [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) and [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) state storages | +| [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) | Config for setting up state syncing with | +| [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) | | + +## Variables + +| Variable | Description | +| --- | --- | +| [createKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md) | Creates [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) state storage | +| [createSessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md) | Creates [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) | + +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [StartSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md) | | +| [StopSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md) | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md new file mode 100644 index 0000000000000..23f71ba330d4b --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [StartSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md) + +## StartSyncStateFnType type + + +Signature: + +```typescript +export declare type StartSyncStateFnType = () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md new file mode 100644 index 0000000000000..69ff6e899e860 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [StopSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md) + +## StopSyncStateFnType type + + +Signature: + +```typescript +export declare type StopSyncStateFnType = () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md new file mode 100644 index 0000000000000..d095c3fffc512 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md @@ -0,0 +1,93 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [syncState](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) + +## syncState() function + +Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL) Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples + +Signature: + +```typescript +export declare function syncState({ storageKey, stateStorage, stateContainer, }: IStateSyncConfig): ISyncStateRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { storageKey, stateStorage, stateContainer, } | IStateSyncConfig<State, IStateStorage> | | + +Returns: + +`ISyncStateRef` + +- [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) + +## Remarks + +1. It is responsibility of consumer to make sure that initial app state and storage are in sync before starting syncing No initial sync happens when syncState() is called + +## Example 1 + +1. the simplest use case + +```ts +const stateStorage = createKbnUrlStateStorage(); +syncState({ + storageKey: '_s', + stateContainer, + stateStorage +}); + +``` + +## Example 2 + +2. conditionally configuring sync strategy + +```ts +const stateStorage = createKbnUrlStateStorage({useHash: config.get('state:stateContainerInSessionStorage')}) +syncState({ + storageKey: '_s', + stateContainer, + stateStorage +}); + +``` + +## Example 3 + +3. implementing custom sync strategy + +```ts +const localStorageStateStorage = { + set: (storageKey, state) => localStorage.setItem(storageKey, JSON.stringify(state)), + get: (storageKey) => localStorage.getItem(storageKey) ? JSON.parse(localStorage.getItem(storageKey)) : null +}; +syncState({ + storageKey: '_s', + stateContainer, + stateStorage: localStorageStateStorage +}); + +``` + +## Example 4 + +4. Transform state before serialising Useful for: \* Migration / backward compatibility \* Syncing part of state \* Providing default values + +```ts +const stateToStorage = (s) => ({ tab: s.tab }); +syncState({ + storageKey: '_s', + stateContainer: { + get: () => stateToStorage(stateContainer.get()), + set: stateContainer.set(({ tab }) => ({ ...stateContainer.get(), tab }), + state$: stateContainer.state$.pipe(map(stateToStorage)) + }, + stateStorage +}); + +``` + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md new file mode 100644 index 0000000000000..87a2449a384df --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md @@ -0,0 +1,42 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [syncStates](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md) + +## syncStates() function + +Signature: + +```typescript +export declare function syncStates(stateSyncConfigs: Array>): ISyncStateRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| stateSyncConfigs | Array<IStateSyncConfig<any>> | Array of [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) to sync | + +Returns: + +`ISyncStateRef` + +## Example + +sync multiple different sync configs + +```ts +syncStates([ + { + storageKey: '_s1', + stateStorage: stateStorage1, + stateContainer: stateContainer1, + }, + { + storageKey: '_s2', + stateStorage: stateStorage2, + stateContainer: stateContainer2, + }, +]); + +``` + diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 0575b8532508f..bffb3f97cd1b9 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -23,8 +23,8 @@ experimental[] To visualize data in a CSV, JSON, or log file, you can upload it using the File Data Visualizer. On the home page, click *Import a CSV, NDSON, or log file*, and then drag your file into the File Data Visualizer. Alternatively, you can open -it by navigating to the Machine Learning app page from the sidebar menu and -selecting the Data Visualizer from the top navigation bar on the opening page. +it by navigating to *Machine Learning* from the side navigation and selecting +*Data Visualizer*. [role="screenshot"] image::images/data-viz-homepage.jpg[File Data Visualizer on the home page] diff --git a/scripts/backport.js b/scripts/backport.js index 64cd5721834ea..2094534e2c4b3 100644 --- a/scripts/backport.js +++ b/scripts/backport.js @@ -18,4 +18,5 @@ */ require('../src/setup_node_env/node_version_validator'); -require('backport'); +var backport = require('backport'); +backport.run(); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index e0afd5e57f041..7520111bf33ac 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -60,7 +60,7 @@ import { } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { UuidServiceSetup } from './uuid'; -import { MetricsServiceSetup } from './metrics'; +import { MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { LoggingServiceSetup, @@ -403,8 +403,6 @@ export interface CoreSetup { contracts: new Map([['plugin-id', 'plugin-value']]), }, rendering: renderingServiceMock, - metrics: metricsServiceMock.createInternalSetupContract(), uuid: uuidSetup, status: statusServiceMock.createInternalSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index be737f6593c02..a544bad6c0e41 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -276,6 +276,9 @@ export class LegacyService implements CoreService { createSerializer: startDeps.core.savedObjects.createSerializer, getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry, }, + metrics: { + getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, + }, uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, }; @@ -312,9 +315,6 @@ export class LegacyService implements CoreService { logging: { configure: (config$) => setupDeps.core.logging.configure([], config$), }, - metrics: { - getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, - }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index cc53a4e27d571..769f6ee2a549a 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -16,29 +16,46 @@ * specific language governing permissions and limitations * under the License. */ - +import { BehaviorSubject } from 'rxjs'; import { MetricsService } from './metrics_service'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart, - MetricsServiceSetup, MetricsServiceStart, } from './types'; -const createSetupContractMock = () => { - const setupContract: jest.Mocked = { - getOpsMetrics$: jest.fn(), - }; - return setupContract; -}; - const createInternalSetupContractMock = () => { - const setupContract: jest.Mocked = createSetupContractMock(); + const setupContract: jest.Mocked = {}; return setupContract; }; const createStartContractMock = () => { - const startContract: jest.Mocked = {}; + const startContract: jest.Mocked = { + getOpsMetrics$: jest.fn(), + }; + startContract.getOpsMetrics$.mockReturnValue( + new BehaviorSubject({ + process: { + memory: { + heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, + resident_set_size_in_bytes: 1, + }, + event_loop_delay: 1, + pid: 1, + uptime_in_millis: 1, + }, + os: { + platform: 'darwin' as const, + platformRelease: 'test', + load: { '1m': 1, '5m': 1, '15m': 1 }, + memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 }, + uptime_in_millis: 1, + }, + response_times: { avg_in_millis: 1, max_in_millis: 1 }, + requests: { disconnects: 1, total: 1, statusCodes: { '200': 1 } }, + concurrent_connections: 1, + }) + ); return startContract; }; @@ -60,7 +77,7 @@ const createMock = () => { export const metricsServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, + createSetupContract: createStartContractMock, createStartContract: createStartContractMock, createInternalSetupContract: createInternalSetupContractMock, createInternalStartContract: createInternalStartContractMock, diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts index b3cc06ffca1d2..f2019de7b6cab 100644 --- a/src/core/server/metrics/metrics_service.test.ts +++ b/src/core/server/metrics/metrics_service.test.ts @@ -75,8 +75,8 @@ describe('MetricsService', () => { it('resets the collector after each collection', async () => { mockOpsCollector.collect.mockResolvedValue(dummyMetrics); - const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); - await metricsService.start(); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); // `advanceTimersByTime` only ensure the interval handler is executed // however the `reset` call is executed after the async call to `collect` @@ -109,8 +109,8 @@ describe('MetricsService', () => { describe('#stop', () => { it('stops the metrics interval', async () => { - const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); - await metricsService.start(); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); @@ -125,8 +125,8 @@ describe('MetricsService', () => { }); it('completes the metrics observable', async () => { - const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); - await metricsService.start(); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); let completed = false; diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts index 0ea9d00792600..f28fb21aaac0d 100644 --- a/src/core/server/metrics/metrics_service.ts +++ b/src/core/server/metrics/metrics_service.ts @@ -45,12 +45,7 @@ export class MetricsService public async setup({ http }: MetricsServiceSetupDeps): Promise { this.metricsCollector = new OpsMetricsCollector(http.server); - - const metricsObservable = this.metrics$.asObservable(); - - return { - getOpsMetrics$: () => metricsObservable, - }; + return {}; } public async start(): Promise { @@ -68,7 +63,11 @@ export class MetricsService this.refreshMetrics(); }, config.interval.asMilliseconds()); - return {}; + const metricsObservable = this.metrics$.asObservable(); + + return { + getOpsMetrics$: () => metricsObservable, + }; } private async refreshMetrics() { diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts index 5c8f18fff380d..cbf0acacd6bab 100644 --- a/src/core/server/metrics/types.ts +++ b/src/core/server/metrics/types.ts @@ -20,12 +20,14 @@ import { Observable } from 'rxjs'; import { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors'; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MetricsServiceSetup {} /** * APIs to retrieves metrics gathered and exposed by the core platform. * * @public */ -export interface MetricsServiceSetup { +export interface MetricsServiceStart { /** * Retrieve an observable emitting the {@link OpsMetrics} gathered. * The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, @@ -40,8 +42,6 @@ export interface MetricsServiceSetup { */ getOpsMetrics$: () => Observable; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MetricsServiceStart {} export type InternalMetricsServiceSetup = MetricsServiceSetup; export type InternalMetricsServiceStart = MetricsServiceStart; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 2ac5bd98f7ed4..4491942951c50 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -129,7 +129,6 @@ function createCoreSetupMock({ http: httpMock, savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createSetupContract(), - metrics: metricsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), logging: loggingServiceMock.createSetupContract(), @@ -146,6 +145,7 @@ function createCoreStartMock() { capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createStart(), http: httpServiceMock.createStartContract(), + metrics: metricsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), }; @@ -159,7 +159,6 @@ function createInternalCoreSetupMock() { context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createInternalSetup(), http: httpServiceMock.createInternalSetupContract(), - metrics: metricsServiceMock.createInternalSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), @@ -176,6 +175,7 @@ function createInternalCoreStartMock() { capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createStart(), http: httpServiceMock.createInternalStartContract(), + metrics: metricsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), }; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 32bc8dc088cad..4643789d99a88 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -169,9 +169,6 @@ export function createPluginSetupContext( logging: { configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$), }, - metrics: { - getOpsMetrics$: deps.metrics.getOpsMetrics$, - }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, addClientWrapper: deps.savedObjects.addClientWrapper, @@ -225,6 +222,9 @@ export function createPluginStartContext( createSerializer: deps.savedObjects.createSerializer, getTypeRegistry: deps.savedObjects.getTypeRegistry, }, + metrics: { + getOpsMetrics$: deps.metrics.getOpsMetrics$, + }, uiSettings: { asScopedToClient: deps.uiSettings.asScopedToClient, }, diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index 8362d1f16bd2a..c037ed733549e 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -145,10 +145,14 @@ export interface SavedObjectsCoreFieldMapping { /** * See {@link SavedObjectsFieldMapping} for documentation. * + * Note: this type intentially doesn't include a type definition for defining + * the `dynamic` mapping parameter. Saved Object fields should always inherit + * the `dynamic: 'strict'` paramater. If you are unsure of the shape of your + * data use `type: 'object', enabled: false` instead. + * * @public */ export interface SavedObjectsComplexFieldMapping { - dynamic?: string; type?: string; properties: SavedObjectsMappingProperties; } diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index c2a7b11e057cd..4561f4d30e104 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -130,6 +130,8 @@ function defaultMapping(): IndexMapping { dynamic: 'strict', properties: { migrationVersion: { + // Saved Objects can't redefine dynamic, but we cheat here to support migrations + // @ts-expect-error dynamic: 'true', type: 'object', }, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4d6316fceb568..108826ad61aa2 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -712,8 +712,6 @@ export interface CoreSetup Observable; } // @public (undocumented) @@ -1970,8 +1974,6 @@ export interface SavedObjectsClientWrapperOptions { // @public export interface SavedObjectsComplexFieldMapping { - // (undocumented) - dynamic?: string; // (undocumented) properties: SavedObjectsMappingProperties; // (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 3bbcd0e37e142..dc37b77c57c92 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -150,7 +150,7 @@ export class Server { savedObjects: savedObjectsSetup, }); - const metricsSetup = await this.metrics.setup({ http: httpSetup }); + await this.metrics.setup({ http: httpSetup }); const renderingSetup = await this.rendering.setup({ http: httpSetup, @@ -181,7 +181,6 @@ export class Server { status: statusSetup, uiSettings: uiSettingsSetup, uuid: uuidSetup, - metrics: metricsSetup, rendering: renderingSetup, httpResources: httpResourcesSetup, logging: loggingSetup, @@ -211,12 +210,14 @@ export class Server { }); const capabilitiesStart = this.capabilities.start(); const uiSettingsStart = await this.uiSettings.start(); + const metricsStart = await this.metrics.start(); const httpStart = this.http.getStartContract(); this.coreStart = { capabilities: capabilitiesStart, elasticsearch: elasticsearchStart, http: httpStart, + metrics: metricsStart, savedObjects: savedObjectsStart, uiSettings: uiSettingsStart, }; @@ -236,7 +237,6 @@ export class Server { await this.rendering.start({ legacy: this.legacy, }); - await this.metrics.start(); return this.coreStart; } diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js index ddc6d000bca19..32eb7bf8712e3 100644 --- a/src/dev/build/tasks/copy_source_task.js +++ b/src/dev/build/tasks/copy_source_task.js @@ -34,9 +34,7 @@ export const CopySourceTask = { '!src/test_utils/**', '!src/fixtures/**', '!src/legacy/core_plugins/tests_bundle/**', - '!src/legacy/core_plugins/testbed/**', '!src/legacy/core_plugins/console/public/tests/**', - '!src/plugins/testbed/**', '!src/cli/cluster/**', '!src/cli/repl/**', '!src/es_archiver/**', diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index 45dafe1b415e3..0aa450c8b002a 100644 --- a/src/dev/run_check_published_api_changes.ts +++ b/src/dev/run_check_published_api_changes.ts @@ -43,7 +43,18 @@ import getopts from 'getopts'; */ const getReportFileName = (folder: string) => { - return folder.indexOf('public') > -1 ? 'public' : 'server'; + switch (true) { + case folder.includes('public'): + return 'public'; + case folder.includes('server'): + return 'server'; + case folder.includes('common'): + return 'common'; + default: + throw new Error( + `folder "${folder}" expected to include one of ["public", "server", "common"]` + ); + } }; const apiExtractorConfig = (folder: string): ExtractorConfig => { @@ -131,7 +142,7 @@ const runApiExtractor = ( messageCallback: (message: ExtractorMessage) => { if (message.messageId === 'console-api-report-not-copied') { // ConsoleMessageId.ApiReportNotCopied - log.warning(`You have changed the signature of the ${folder} Core API`); + log.warning(`You have changed the signature of the ${folder} public API`); log.warning( 'To accept these changes run `node scripts/check_published_api_changes.js --accept` and then:\n' + "\t 1. Commit the updated documentation and API review file '" + @@ -142,7 +153,7 @@ const runApiExtractor = ( message.handled = true; } else if (message.messageId === 'console-api-report-copied') { // ConsoleMessageId.ApiReportCopied - log.warning(`You have changed the signature of the ${folder} Core API`); + log.warning(`You have changed the signature of the ${folder} public API`); log.warning( "Please commit the updated API documentation and the API review file: '" + config.reportFilePath @@ -150,7 +161,7 @@ const runApiExtractor = ( message.handled = true; } else if (message.messageId === 'console-api-report-unchanged') { // ConsoleMessageId.ApiReportUnchanged - log.info(`Core ${folder} API: no changes detected ✔`); + log.info(`${folder} API: no changes detected ✔`); message.handled = true; } }, @@ -170,7 +181,7 @@ async function run( folder: string, { log, opts }: { log: ToolingLog; opts: Options } ): Promise { - log.info(`Core ${folder} API: checking for changes in API signature...`); + log.info(`${folder} API: checking for changes in API signature...`); const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); @@ -188,7 +199,7 @@ async function run( log.error(e); return false; } - log.info(`Core ${folder} API: updated documentation ✔`); + log.info(`${folder} API: updated documentation ✔`); } // If the api signature changed or any errors or warnings occured, exit with an error @@ -224,24 +235,31 @@ async function run( opts.help = true; } - const folders = ['core/public', 'core/server', 'plugins/data/server', 'plugins/data/public']; + const core = ['core/public', 'core/server']; + const plugins = [ + 'plugins/data/server', + 'plugins/data/public', + 'plugins/kibana_utils/common/state_containers', + 'plugins/kibana_utils/public/state_sync', + ]; + const folders = [...core, ...plugins]; if (opts.help) { process.stdout.write( dedent(chalk` {dim usage:} node scripts/check_published_api_changes [...options] - Checks for any changes to the Kibana Core API + Checks for any changes to the Kibana shared API Examples: - {dim # Checks for any changes to the Kibana Core API} + {dim # Checks for any changes to the Kibana shared API} {dim $} node scripts/check_published_api_changes - {dim # Checks for any changes to the Kibana Core API and updates the documentation} + {dim # Checks for any changes to the Kibana shared API and updates the documentation} {dim $} node scripts/check_published_api_changes --docs - {dim # Checks for and automatically accepts and updates documentation for any changes to the Kibana Core API} + {dim # Checks for and automatically accepts and updates documentation for any changes to the Kibana shared API} {dim $} node scripts/check_published_api_changes --accept {dim # Only checks the core/public directory} @@ -249,7 +267,7 @@ async function run( Options: --accept {dim Accepts all changes by updating the API Review files and documentation} - --docs {dim Updates the Core API documentation} + --docs {dim Updates the API documentation} --filter {dim RegExp that folder names must match, folders: [${folders.join(', ')}]} --help {dim Show this message} `) @@ -259,20 +277,22 @@ async function run( } try { - log.info(`Core: Building types...`); + log.info(`Building types for api extractor...`); await runBuildTypes(); } catch (e) { log.error(e); return false; } - const results = await Promise.all( - folders - .filter((folder) => (opts.filter.length ? folder.match(opts.filter) : true)) - .map((folder) => run(folder, { log, opts })) + const filteredFolders = folders.filter((folder) => + opts.filter.length ? folder.match(opts.filter) : true ); + const results = []; + for (const folder of filteredFolders) { + results.push(await run(folder, { log, opts })); + } - if (results.find((r) => r === false) !== undefined) { + if (results.includes(false)) { process.exitCode = 1; } })().catch((e) => { diff --git a/src/legacy/core_plugins/testbed/README.md b/src/legacy/core_plugins/testbed/README.md deleted file mode 100644 index ac50ffbb804b5..0000000000000 --- a/src/legacy/core_plugins/testbed/README.md +++ /dev/null @@ -1,8 +0,0 @@ -## Kibana Testbed - -Sometimes when developing for Kibana, it is useful to have an isolated routable space to demonstrate new functionality. This Testbed provides such a space. - -To make use of the testbed, edit the testbed.js, testbed.html, and testbed.less files as necessary. When you are done demonstrating -your new functionality, remember to cleanup your changes and restore the testbed to its pristine state for the next person. - -To access the testbed, visit `http://localhost:5601/app/kibana#/testbed` diff --git a/src/legacy/core_plugins/testbed/package.json b/src/legacy/core_plugins/testbed/package.json deleted file mode 100644 index 98fcaf7eda95d..0000000000000 --- a/src/legacy/core_plugins/testbed/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "testbed", - "version": "kibana" -} \ No newline at end of file diff --git a/src/legacy/core_plugins/testbed/public/testbed.html b/src/legacy/core_plugins/testbed/public/testbed.html deleted file mode 100644 index 52455beb02360..0000000000000 --- a/src/legacy/core_plugins/testbed/public/testbed.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
- -
{{ testbed.data }}
- - - - - - -
-
diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 09957e61f74d3..0221c7e0ea085 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -54,7 +54,7 @@ export function registerStatsApi(usageCollection, server, config, kbnServer) { /* kibana_stats gets singled out from the collector set as it is used * for health-checking Kibana and fetch does not rely on fetching data * from ES */ - server.newPlatform.setup.core.metrics.getOpsMetrics$().subscribe((metrics) => { + server.newPlatform.start.core.metrics.getOpsMetrics$().subscribe((metrics) => { lastMetrics = { ...metrics, timestamp: new Date().toISOString(), diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 3c559a6cde211..b52bf5bf02b7b 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -60,6 +60,7 @@ import { ViewMode, SavedObjectEmbeddableInput, ContainerOutput, + EmbeddableInput, } from '../../../embeddable/public'; import { NavAction, SavedDashboardPanel } from '../types'; @@ -430,9 +431,16 @@ export class DashboardAppController { .getStateTransfer(scopedHistory()) .getIncomingEmbeddablePackage(); if (incomingState) { - container.addNewEmbeddable(incomingState.type, { - savedObjectId: incomingState.id, - }); + if ('id' in incomingState) { + container.addNewEmbeddable(incomingState.type, { + savedObjectId: incomingState.id, + }); + } else if ('input' in incomingState) { + container.addNewEmbeddable( + incomingState.type, + incomingState.input + ); + } } } diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts index ff6dff9d8b749..e3099ae6a4026 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/src/plugins/data/common/es_query/filters/meta_filter.ts @@ -107,3 +107,13 @@ export const pinFilter = (filter: Filter) => export const unpinFilter = (filter: Filter) => !isFilterPinned(filter) ? filter : toggleFilterPinned(filter); + +export const isFilter = (x: unknown): x is Filter => + !!x && + typeof x === 'object' && + !!(x as Filter).meta && + typeof (x as Filter).meta === 'object' && + typeof (x as Filter).meta.disabled === 'boolean'; + +export const isFilters = (x: unknown): x is Filter[] => + Array.isArray(x) && !x.find((y) => !isFilter(y)); diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index adbd93d518fc7..b40e02b709d30 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -20,11 +20,12 @@ export * from './constants'; export * from './es_query'; export * from './field_formats'; +export * from './field_mapping'; export * from './index_patterns'; export * from './kbn_field_types'; export * from './query'; export * from './search'; export * from './search/aggs'; +export * from './timefilter'; export * from './types'; export * from './utils'; -export * from './field_mapping'; diff --git a/src/plugins/data/common/query/index.ts b/src/plugins/data/common/query/index.ts index 421cc4f63e4ef..4e90f6f8bb83e 100644 --- a/src/plugins/data/common/query/index.ts +++ b/src/plugins/data/common/query/index.ts @@ -19,3 +19,4 @@ export * from './filter_manager'; export * from './types'; +export * from './is_query'; diff --git a/src/legacy/core_plugins/testbed/index.js b/src/plugins/data/common/query/is_query.ts similarity index 73% rename from src/legacy/core_plugins/testbed/index.js rename to src/plugins/data/common/query/is_query.ts index f0b61ea0c3de7..08a99a39b1ac1 100644 --- a/src/legacy/core_plugins/testbed/index.js +++ b/src/plugins/data/common/query/is_query.ts @@ -17,14 +17,11 @@ * under the License. */ -import { resolve } from 'path'; +import { Query } from './types'; -export default function (kibana) { - return new kibana.Plugin({ - id: 'testbed', - publicDir: resolve(__dirname, 'public'), - uiExports: { - hacks: ['plugins/testbed'], - }, - }); -} +export const isQuery = (x: unknown): x is Query => + !!x && + typeof x === 'object' && + typeof (x as Query).language === 'string' && + (typeof (x as Query).query === 'string' || + (typeof (x as Query).query === 'object' && !!(x as Query).query)); diff --git a/src/legacy/core_plugins/testbed/public/index.js b/src/plugins/data/common/timefilter/index.ts similarity index 94% rename from src/legacy/core_plugins/testbed/public/index.js rename to src/plugins/data/common/timefilter/index.ts index c6687de249cf2..e0c509e119fda 100644 --- a/src/legacy/core_plugins/testbed/public/index.js +++ b/src/plugins/data/common/timefilter/index.ts @@ -17,4 +17,4 @@ * under the License. */ -import './testbed'; +export { isTimeRange } from './is_time_range'; diff --git a/src/legacy/core_plugins/testbed/public/testbed.js b/src/plugins/data/common/timefilter/is_time_range.ts similarity index 78% rename from src/legacy/core_plugins/testbed/public/testbed.js rename to src/plugins/data/common/timefilter/is_time_range.ts index 13005a6106ca4..f206cd04dde31 100644 --- a/src/legacy/core_plugins/testbed/public/testbed.js +++ b/src/plugins/data/common/timefilter/is_time_range.ts @@ -17,13 +17,10 @@ * under the License. */ -import uiRoutes from 'ui/routes'; -import template from './testbed.html'; +import { TimeRange } from './types'; -uiRoutes.when('/testbed', { - template: template, - controllerAs: 'testbed', - controller: class TestbedController { - constructor() {} - }, -}); +export const isTimeRange = (x: unknown): x is TimeRange => + !!x && + typeof x === 'object' && + typeof (x as TimeRange).from === 'string' && + typeof (x as TimeRange).to === 'string'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 3665d9dc2b46e..efce8d2c021c9 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -440,6 +440,8 @@ export { getKbnTypeNames, } from '../common'; +export { isTimeRange, isQuery, isFilter, isFilters } from '../common'; + export * from '../common/field_mapping'; /* diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 25c9b0718050a..b12ad94017fbb 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1291,6 +1291,26 @@ export interface ISearchStrategy { search: ISearch; } +// 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) +export const isFilter: (x: unknown) => x is Filter; + +// Warning: (ae-missing-release-tag) "isFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isFilters: (x: unknown) => x is Filter[]; + +// Warning: (ae-missing-release-tag) "isQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isQuery: (x: unknown) => x is Query; + +// Warning: (ae-missing-release-tag) "isTimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isTimeRange: (x: unknown) => x is TimeRange; + // Warning: (ae-missing-release-tag) "ISyncSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx index 8b646106fe52f..9afe5e48bc5b8 100644 --- a/src/plugins/discover/public/application/angular/directives/histogram.tsx +++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx @@ -323,6 +323,7 @@ export class DiscoverHistogram extends Component { + test('validates given time ranges correctly', async () => { + const { toasts } = notificationServiceMock.createStartContract(); + [ + { from: '', to: '', result: false }, + { from: 'now', to: 'now+1h', result: true }, + { from: 'now', to: 'lala+1h', result: false }, + { from: '', to: 'now', result: false }, + { from: 'now', to: '', result: false }, + { from: ' 2020-06-02T13:36:13.689Z', to: 'now', result: true }, + { from: ' 2020-06-02T13:36:13.689Z', to: '2020-06-02T13:36:13.690Z', result: true }, + ].map((test) => { + expect(validateTimeRange({ from: test.from, to: test.to }, toasts)).toEqual(test.result); + }); + }); + + test('displays a toast when invalid data is entered', async () => { + const { toasts } = notificationServiceMock.createStartContract(); + expect(validateTimeRange({ from: 'now', to: 'null' }, toasts)).toEqual(false); + expect(toasts.addDanger).toHaveBeenCalledWith({ + title: 'Invalid time range', + text: "The provided time range is invalid. (from: 'now', to: 'null')", + }); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/validate_time_range.ts b/src/plugins/discover/public/application/helpers/validate_time_range.ts new file mode 100644 index 0000000000000..411147f827333 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/validate_time_range.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import dateMath from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; +import { ToastsStart } from 'kibana/public'; + +/** + * Validates a given time filter range, provided by URL or UI + * Unless valid, it returns false and displays a notification + */ +export function validateTimeRange( + { from, to }: { from: string; to: string }, + toastNotifications: ToastsStart +): boolean { + const fromMoment = dateMath.parse(from); + const toMoment = dateMath.parse(to); + if (!fromMoment || !toMoment || !fromMoment.isValid() || !toMoment.isValid()) { + toastNotifications.addDanger({ + title: i18n.translate('discover.notifications.invalidTimeRangeTitle', { + defaultMessage: `Invalid time range`, + }), + text: i18n.translate('discover.notifications.invalidTimeRangeText', { + defaultMessage: `The provided time range is invalid. (from: '{from}', to: '{to}')`, + values: { + from, + to, + }, + }), + }); + return false; + } + return true; +} diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 4154fdfeb3ff4..6ac8f674b6153 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -27,4 +27,4 @@ export function plugin(initializerContext: PluginInitializerContext) { export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; -export { DISCOVER_APP_URL_GENERATOR } from './url_generator'; +export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index cca63cd880b60..2c6bbcc3ecce1 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -60,10 +60,23 @@ export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter createHashHistory()); +/** + * Discover currently uses two `history` instances: one from Kibana Platform and + * another from `history` package. Below function is used every time Discover + * app is loaded to synchronize both instances. + * + * This helper is temporary until https://github.com/elastic/kibana/issues/65161 is resolved. + */ +export const syncHistoryLocations = () => { + const h = getHistory(); + Object.assign(h.location, createHashHistory().location); + return h; +}; + export const [getScopedHistory, setScopedHistory] = createGetterSetter( 'scopedHistory' ); diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index ba97efa55068d..e97ac783c616f 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -55,6 +55,7 @@ import { setServices, setScopedHistory, getScopedHistory, + syncHistoryLocations, getServices, } from './kibana_services'; import { createSavedSearchesLoader } from './saved_searches'; @@ -245,6 +246,7 @@ export class DiscoverPlugin throw Error('Discover plugin method initializeInnerAngular is undefined'); } setScopedHistory(params.history); + syncHistoryLocations(); appMounted(); const { plugins: { data: dataStart }, diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index 42d689050d5ad..c7f2e2147e819 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -98,11 +98,13 @@ export class DiscoverUrlGenerator const queryState: QueryState = {}; if (query) appState.query = query; - if (filters) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + if (filters && filters.length) + appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); if (indexPatternId) appState.index = indexPatternId; if (timeRange) queryState.time = timeRange; - if (filters) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (filters && filters.length) + queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); if (refreshInterval) queryState.refreshInterval = refreshInterval; let url = `${this.params.appBasePath}#/${savedSearchPath}`; diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 06b0e88da334f..332237d19e218 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -4,6 +4,7 @@ "server": false, "ui": true, "requiredPlugins": [ + "data", "inspector", "uiActions" ], diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 1d1dc79121937..f19974942c43d 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -28,6 +28,7 @@ export { ACTION_EDIT_PANEL, Adapters, AddPanelAction, + ChartActionContext, Container, ContainerInput, ContainerOutput, @@ -69,7 +70,7 @@ export { isRangeSelectTriggerContext, isValueClickTriggerContext, EmbeddableStateTransfer, - EmbeddableOriginatingAppState, + EmbeddableEditorState, EmbeddablePackageState, EmbeddableRenderer, EmbeddableRendererProps, diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 4b602efb02717..594a7ad73c396 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -59,7 +59,7 @@ test('redirects to app using state transfer', async () => { const embeddable = new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true); embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' })); await action.execute({ embeddable }); - expect(stateTransferMock.navigateToWithOriginatingApp).toHaveBeenCalledWith('ultraVisualize', { + expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', { path: '/123', state: { originatingApp: 'superCoolCurrentApp' }, }); diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index d983dc9f41853..9177a77d547b0 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -24,7 +24,7 @@ import { take } from 'rxjs/operators'; import { ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; -import { IEmbeddable, EmbeddableOriginatingAppState, EmbeddableStateTransfer } from '../..'; +import { IEmbeddable, EmbeddableEditorState, EmbeddableStateTransfer } from '../..'; export const ACTION_EDIT_PANEL = 'editPanel'; @@ -35,7 +35,7 @@ interface ActionContext { interface NavigationContext { app: string; path: string; - state?: EmbeddableOriginatingAppState; + state?: EmbeddableEditorState; } export class EditPanelAction implements Action { @@ -88,7 +88,7 @@ export class EditPanelAction implements Action { const appTarget = this.getAppTarget(context); if (appTarget) { if (this.stateTransfer && appTarget.state) { - await this.stateTransfer.navigateToWithOriginatingApp(appTarget.app, { + await this.stateTransfer.navigateToEditor(appTarget.app, { path: appTarget.path, state: appTarget.state, }); diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 9c544e86e189a..fcecf117d7d52 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -51,8 +51,7 @@ export abstract class Embeddable< // to update input when the parent changes. private parentSubscription?: Rx.Subscription; - // TODO: Rename to destroyed. - private destoyed: boolean = false; + private destroyed: boolean = false; constructor(input: TEmbeddableInput, output: TEmbeddableOutput, parent?: IContainer) { this.id = input.id; @@ -123,7 +122,7 @@ export abstract class Embeddable< } public updateInput(changes: Partial): void { - if (this.destoyed) { + if (this.destroyed) { throw new Error('Embeddable has been destroyed'); } if (this.parent) { @@ -135,7 +134,7 @@ export abstract class Embeddable< } public render(domNode: HTMLElement | Element): void { - if (this.destoyed) { + if (this.destroyed) { throw new Error('Embeddable has been destroyed'); } return; @@ -155,7 +154,7 @@ export abstract class Embeddable< * implementors to add any additional clean up tasks, like unmounting and unsubscribing. */ public destroy(): void { - this.destoyed = true; + this.destroyed = true; this.input$.complete(); this.output$.complete(); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts index 0d5ae6be68185..b7dd95ccba32c 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts @@ -38,7 +38,7 @@ describe('embeddable state transfer', () => { }); it('can send an outgoing originating app state', async () => { - await stateTransfer.navigateToWithOriginatingApp(destinationApp, { state: { originatingApp } }); + await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { state: { originatingApp: 'superUltraTestDashboard' }, }); @@ -50,7 +50,7 @@ describe('embeddable state transfer', () => { application.navigateToApp, (historyMock as unknown) as ScopedHistory ); - await stateTransfer.navigateToWithOriginatingApp(destinationApp, { + await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp }, appendToExistingState: true, }); @@ -94,7 +94,7 @@ describe('embeddable state transfer', () => { application.navigateToApp, (historyMock as unknown) as ScopedHistory ); - const fetchedState = stateTransfer.getIncomingOriginatingApp(); + const fetchedState = stateTransfer.getIncomingEditorState(); expect(fetchedState).toEqual({ originatingApp: 'extremeSportsKibana' }); }); @@ -104,7 +104,7 @@ describe('embeddable state transfer', () => { application.navigateToApp, (historyMock as unknown) as ScopedHistory ); - const fetchedState = stateTransfer.getIncomingOriginatingApp(); + const fetchedState = stateTransfer.getIncomingEditorState(); expect(fetchedState).toBeUndefined(); }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index 57b425d2df45c..8f70e5a66c478 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -20,8 +20,8 @@ import { cloneDeep } from 'lodash'; import { ScopedHistory, ApplicationStart } from '../../../../../core/public'; import { - EmbeddableOriginatingAppState, - isEmbeddableOriginatingAppState, + EmbeddableEditorState, + isEmbeddableEditorState, EmbeddablePackageState, isEmbeddablePackageState, } from './types'; @@ -39,16 +39,16 @@ export class EmbeddableStateTransfer { ) {} /** - * Fetches an {@link EmbeddableOriginatingAppState | originating app} argument from the scoped + * Fetches an {@link EmbeddableEditorState | originating app} argument from the scoped * history's location state. * * @param history - the scoped history to fetch from * @param options.keysToRemoveAfterFetch - an array of keys to be removed from the state after they are retrieved */ - public getIncomingOriginatingApp(options?: { + public getIncomingEditorState(options?: { keysToRemoveAfterFetch?: string[]; - }): EmbeddableOriginatingAppState | undefined { - return this.getIncomingState(isEmbeddableOriginatingAppState, { + }): EmbeddableEditorState | undefined { + return this.getIncomingState(isEmbeddableEditorState, { keysToRemoveAfterFetch: options?.keysToRemoveAfterFetch, }); } @@ -70,17 +70,17 @@ export class EmbeddableStateTransfer { /** * A wrapper around the {@link ApplicationStart.navigateToApp} method which navigates to the specified appId - * with {@link EmbeddableOriginatingAppState | originating app state} + * with {@link EmbeddableEditorState | embeddable editor state} */ - public async navigateToWithOriginatingApp( + public async navigateToEditor( appId: string, options?: { path?: string; - state: EmbeddableOriginatingAppState; + state: EmbeddableEditorState; appendToExistingState?: boolean; } ): Promise { - await this.navigateToWithState(appId, options); + await this.navigateToWithState(appId, options); } /** diff --git a/src/plugins/embeddable/public/lib/state_transfer/index.ts b/src/plugins/embeddable/public/lib/state_transfer/index.ts index e51efc5dcca26..7daa7a0ea81d6 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/index.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/index.ts @@ -18,4 +18,4 @@ */ export { EmbeddableStateTransfer } from './embeddable_state_transfer'; -export { EmbeddableOriginatingAppState, EmbeddablePackageState } from './types'; +export { EmbeddableEditorState, EmbeddablePackageState } from './types'; diff --git a/src/plugins/embeddable/public/lib/state_transfer/types.ts b/src/plugins/embeddable/public/lib/state_transfer/types.ts index 8eae441d1be23..a6721784302ac 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/types.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/types.ts @@ -17,33 +17,49 @@ * under the License. */ +import { EmbeddableInput } from '..'; + /** * Represents a state package that contains the last active app id. * @public */ -export interface EmbeddableOriginatingAppState { +export interface EmbeddableEditorState { originatingApp: string; + byValueMode?: boolean; + valueInput?: EmbeddableInput; } -export function isEmbeddableOriginatingAppState( - state: unknown -): state is EmbeddableOriginatingAppState { +export function isEmbeddableEditorState(state: unknown): state is EmbeddableEditorState { return ensureFieldOfTypeExists('originatingApp', state, 'string'); } /** - * Represents a state package that contains all fields necessary to create an embeddable in a container. + * Represents a state package that contains all fields necessary to create an embeddable by reference in a container. * @public */ -export interface EmbeddablePackageState { +export interface EmbeddablePackageByReferenceState { type: string; id: string; } +/** + * Represents a state package that contains all fields necessary to create an embeddable by value in a container. + * @public + */ +export interface EmbeddablePackageByValueState { + type: string; + input: EmbeddableInput; +} + +export type EmbeddablePackageState = + | EmbeddablePackageByReferenceState + | EmbeddablePackageByValueState; + export function isEmbeddablePackageState(state: unknown): state is EmbeddablePackageState { return ( - ensureFieldOfTypeExists('type', state, 'string') && - ensureFieldOfTypeExists('id', state, 'string') + (ensureFieldOfTypeExists('type', state, 'string') && + ensureFieldOfTypeExists('id', state, 'string')) || + ensureFieldOfTypeExists('input', state, 'object') ); } diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 2b447c89e2850..5bb96a708b7ac 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -39,10 +39,6 @@ export interface ValueClickTriggerContext { }; } -export const isValueClickTriggerContext = ( - context: ValueClickTriggerContext | RangeSelectTriggerContext -): context is ValueClickTriggerContext => context.data && 'data' in context.data; - export interface RangeSelectTriggerContext { embeddable?: T; data: { @@ -53,8 +49,16 @@ export interface RangeSelectTriggerContext }; } +export type ChartActionContext = + | ValueClickTriggerContext + | RangeSelectTriggerContext; + +export const isValueClickTriggerContext = ( + context: ChartActionContext +): context is ValueClickTriggerContext => context.data && 'data' in context.data; + export const isRangeSelectTriggerContext = ( - context: ValueClickTriggerContext | RangeSelectTriggerContext + context: ChartActionContext ): context is RangeSelectTriggerContext => context.data && 'range' in context.data; export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 49910525c7ab1..efd0ccdc4553d 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -31,6 +31,7 @@ import { coreMock } from '../../../core/public/mocks'; import { UiActionsService } from './lib/ui_actions'; import { CoreStart } from '../../../core/public'; import { Start as InspectorStart } from '../../inspector/public'; +import { dataPluginMock } from '../../data/public/mocks'; // eslint-disable-next-line import { inspectorPluginMock } from '../../inspector/public/mocks'; @@ -78,9 +79,9 @@ export const createEmbeddablePanelMock = ({ export const createEmbeddableStateTransferMock = (): Partial => { return { - getIncomingOriginatingApp: jest.fn(), + getIncomingEditorState: jest.fn(), getIncomingEmbeddablePackage: jest.fn(), - navigateToWithOriginatingApp: jest.fn(), + navigateToEditor: jest.fn(), navigateToWithEmbeddablePackage: jest.fn(), }; }; @@ -100,6 +101,8 @@ const createStartContract = (): Start => { EmbeddablePanel: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), + filtersAndTimeRangeFromContext: jest.fn(), + filtersFromContext: jest.fn(), }; return startContract; }; @@ -108,11 +111,13 @@ const createInstance = (setupPlugins: Partial = {}) const plugin = new EmbeddablePublicPlugin({} as any); const setup = plugin.setup(coreMock.createSetup(), { uiActions: setupPlugins.uiActions || uiActionsPluginMock.createSetupContract(), + data: dataPluginMock.createSetupContract(), }); const doStart = (startPlugins: Partial = {}) => plugin.start(coreMock.createStart(), { uiActions: startPlugins.uiActions || uiActionsPluginMock.createStartContract(), inspector: inspectorPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), }); return { plugin, diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index c4e0ca44a4e7e..03bb4a4779267 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -17,6 +17,13 @@ * under the License. */ import React from 'react'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, + Filter, + TimeRange, + esFilters, +} from '../../data/public'; import { getSavedObjectFinder } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; @@ -36,15 +43,20 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, + ChartActionContext, + isRangeSelectTriggerContext, + isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; export interface EmbeddableSetupDependencies { + data: DataPublicPluginSetup; uiActions: UiActionsSetup; } export interface EmbeddableStartDependencies { + data: DataPublicPluginStart; uiActions: UiActionsStart; inspector: InspectorStart; } @@ -70,6 +82,19 @@ export interface EmbeddableStart { embeddableFactoryId: string ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; + + /** + * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries. + */ + filtersFromContext: (context: ChartActionContext) => Promise; + + /** + * Returns possible time range and filters that can be constructed from {@link ChartActionContext} object. + */ + filtersAndTimeRangeFromContext: ( + context: ChartActionContext + ) => Promise<{ filters: Filter[]; timeRange?: TimeRange }>; + EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -107,7 +132,7 @@ export class EmbeddablePublicPlugin implements Plugin { this.embeddableFactories.set( @@ -121,6 +146,41 @@ export class EmbeddablePublicPlugin implements Plugin { + try { + if (isRangeSelectTriggerContext(context)) + return await data.actions.createFiltersFromRangeSelectAction(context.data); + if (isValueClickTriggerContext(context)) + return await data.actions.createFiltersFromValueClickAction(context.data); + // eslint-disable-next-line no-console + console.warn("Can't extract filters from action.", context); + } catch (error) { + // eslint-disable-next-line no-console + console.warn('Error extracting filters from action. Returning empty filter list.', error); + } + return []; + }; + + const filtersAndTimeRangeFromContext: EmbeddableStart['filtersAndTimeRangeFromContext'] = async ( + context + ) => { + const filters = await filtersFromContext(context); + + if (!context.data.timeFieldName) return { filters }; + + const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( + context.data.timeFieldName, + filters + ); + + return { + filters: restOfFilters, + timeRange: timeRangeFilter + ? esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter) + : undefined, + }; + }; + const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({ embeddable, hideHeader, @@ -146,6 +206,8 @@ export class EmbeddablePublicPlugin implements Plugin { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history) diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index e13a906e30338..bb12e3d7b9011 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -23,6 +23,7 @@ import { UiActionsStart } from '../../../ui_actions/public'; import { uiActionsPluginMock } from '../../../ui_actions/public/mocks'; // eslint-disable-next-line import { inspectorPluginMock } from '../../../inspector/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; import { coreMock } from '../../../../core/public/mocks'; import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin'; @@ -42,7 +43,10 @@ export const testPlugin = ( const uiActions = uiActionsPluginMock.createPlugin(coreSetup, coreStart); const initializerContext = {} as any; const plugin = new EmbeddablePublicPlugin(initializerContext); - const setup = plugin.setup(coreSetup, { uiActions: uiActions.setup }); + const setup = plugin.setup(coreSetup, { + data: dataPluginMock.createSetupContract(), + uiActions: uiActions.setup, + }); return { plugin, @@ -51,8 +55,9 @@ export const testPlugin = ( setup, doStart: (anotherCoreStart: CoreStart = coreStart) => { const start = plugin.start(anotherCoreStart, { - uiActions: uiActionsPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), inspector: inspectorPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), }); return start; }, diff --git a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap index 41c4c33b53c8d..f07912eff02b7 100644 --- a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap +++ b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`kibana_usage_collection Runs the setup method without issues 1`] = `true`; +exports[`kibana_usage_collection Runs the setup method without issues 1`] = `false`; exports[`kibana_usage_collection Runs the setup method without issues 2`] = `true`; diff --git a/src/plugins/kibana_usage_collection/server/index.test.ts b/src/plugins/kibana_usage_collection/server/index.test.ts index c2680fef01caa..d4b065896c88c 100644 --- a/src/plugins/kibana_usage_collection/server/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/index.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { BehaviorSubject } from 'rxjs'; import { coreMock, savedObjectsRepositoryMock, @@ -47,30 +46,6 @@ describe('kibana_usage_collection', () => { test('Runs the setup method without issues', () => { const coreSetup = coreMock.createSetup(); - coreSetup.metrics.getOpsMetrics$.mockImplementation( - () => - new BehaviorSubject({ - process: { - memory: { - heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, - resident_set_size_in_bytes: 1, - }, - event_loop_delay: 1, - pid: 1, - uptime_in_millis: 1, - }, - os: { - platform: 'darwin' as const, - platformRelease: 'test', - load: { '1m': 1, '5m': 1, '15m': 1 }, - memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 }, - uptime_in_millis: 1, - }, - response_times: { avg_in_millis: 1, max_in_millis: 1 }, - requests: { disconnects: 1, total: 1, statusCodes: { '200': 1 } }, - concurrent_connections: 1, - }) - ); expect(pluginInstance.setup(coreSetup, { usageCollection })).toBe(undefined); usageCollectors.forEach(({ isReady }) => { @@ -86,6 +61,7 @@ describe('kibana_usage_collection', () => { coreStart.uiSettings.asScopedToClient.mockImplementation(() => uiSettingsServiceMock.createClient() ); + expect(pluginInstance.start(coreStart)).toBe(undefined); usageCollectors.forEach(({ isReady }) => { expect(isReady()).toBe(true); // All should return true at this point diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 64d5367100236..803a9146bd08f 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -18,18 +18,18 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { Observable } from 'rxjs'; +import { Subject, Observable } from 'rxjs'; import { PluginInitializerContext, CoreSetup, Plugin, - MetricsServiceSetup, ISavedObjectsRepository, IUiSettingsClient, SharedGlobalConfig, SavedObjectsClient, CoreStart, SavedObjectsServiceSetup, + OpsMetrics, } from '../../../core/server'; import { registerApplicationUsageCollector, @@ -49,16 +49,18 @@ export class KibanaUsageCollectionPlugin implements Plugin { private readonly legacyConfig$: Observable; private savedObjectsClient?: ISavedObjectsRepository; private uiSettingsClient?: IUiSettingsClient; + private metric$: Subject; constructor(initializerContext: PluginInitializerContext) { this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; + this.metric$ = new Subject(); } public setup( - { savedObjects, metrics, getStartServices }: CoreSetup, + { savedObjects }: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup ) { - this.registerUsageCollectors(usageCollection, metrics, (opts) => + this.registerUsageCollectors(usageCollection, this.metric$, (opts) => savedObjects.registerType(opts) ); } @@ -68,19 +70,22 @@ export class KibanaUsageCollectionPlugin implements Plugin { this.savedObjectsClient = savedObjects.createInternalRepository(); const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient); this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + core.metrics.getOpsMetrics$().subscribe(this.metric$); } - public stop() {} + public stop() { + this.metric$.complete(); + } private registerUsageCollectors( usageCollection: UsageCollectionSetup, - metrics: MetricsServiceSetup, + metric$: Subject, registerType: SavedObjectsRegisterType ) { const getSavedObjectsClient = () => this.savedObjectsClient; const getUiSettingsClient = () => this.uiSettingsClient; - registerOpsStatsCollector(usageCollection, metrics.getOpsMetrics$()); + registerOpsStatsCollector(usageCollection, metric$); registerKibanaUsageCollector(usageCollection, this.legacyConfig$); registerManagementUsageCollector(usageCollection, getUiSettingsClient); registerUiMetricUsageCollector(usageCollection, registerType, getSavedObjectsClient); diff --git a/src/plugins/kibana_utils/common/state_containers/common.api.md b/src/plugins/kibana_utils/common/state_containers/common.api.md new file mode 100644 index 0000000000000..f85458499b719 --- /dev/null +++ b/src/plugins/kibana_utils/common/state_containers/common.api.md @@ -0,0 +1,156 @@ +## API Report File for "kibana" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ComponentType } from 'react'; +import { Ensure } from '@kbn/utility-types'; +import { FC } from 'react'; +import { Observable } from 'rxjs'; +import React from 'react'; + +// @public +export type BaseState = object; + +// @public +export interface BaseStateContainer { + get: () => State; + set: (state: State) => void; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Observable" + state$: Observable; +} + +// @public +export type Comparator = (previous: Result, current: Result) => boolean; + +// @public +export type Connect = (mapStateToProp: MapStateToProps>) => (component: ComponentType) => FC>; + +// @public +export function createStateContainer(defaultState: State): ReduxLikeStateContainer; + +// @public +export function createStateContainer(defaultState: State, pureTransitions: PureTransitions): ReduxLikeStateContainer; + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "PureSelectors" +// +// @public +export function createStateContainer(defaultState: State, pureTransitions: PureTransitions, pureSelectors: PureSelectors, options?: CreateStateContainerOptions): ReduxLikeStateContainer; + +// @public +export interface CreateStateContainerOptions { + freeze?: (state: T) => T; +} + +// @public +export const createStateContainerReactHelpers: >() => { + Provider: React.Provider; + Consumer: React.Consumer; + context: React.Context; + useContainer: () => Container; + useState: () => UnboxState; + useTransitions: () => Container["transitions"]; + useSelector: (selector: (state: UnboxState) => Result, comparator?: Comparator) => Result; + connect: Connect>; +}; + +// @public +export type Dispatch = (action: T) => void; + +// @public (undocumented) +export type EnsurePureSelector = Ensure>; + +// Warning: (ae-incompatible-release-tags) The symbol "EnsurePureTransition" is marked as @public, but its signature references "PureTransition" which is marked as @internal +// +// @public (undocumented) +export type EnsurePureTransition = Ensure>; + +// @public +export type MapStateToProps = (state: State) => StateProps; + +// Warning: (ae-incompatible-release-tags) The symbol "Middleware" is marked as @public, but its signature references "TransitionDescription" which is marked as @internal +// +// @public +export type Middleware = (store: Pick, 'getState' | 'dispatch'>) => (next: (action: TransitionDescription) => TransitionDescription | any) => Dispatch; + +// @public (undocumented) +export type PureSelector = (state: State) => Selector; + +// @public (undocumented) +export type PureSelectorsToSelectors = { + [K in keyof T]: PureSelectorToSelector>; +}; + +// @public (undocumented) +export type PureSelectorToSelector> = ReturnType>; + +// @internal (undocumented) +export type PureTransition = (state: State) => Transition; + +// @internal (undocumented) +export type PureTransitionsToTransitions = { + [K in keyof T]: PureTransitionToTransition>; +}; + +// @internal (undocumented) +export type PureTransitionToTransition> = ReturnType; + +// Warning: (ae-incompatible-release-tags) The symbol "Reducer" is marked as @public, but its signature references "TransitionDescription" which is marked as @internal +// +// @public +export type Reducer = (state: State, action: TransitionDescription) => State; + +// @public +export interface ReduxLikeStateContainer extends StateContainer { + // (undocumented) + addMiddleware: (middleware: Middleware) => void; + // Warning: (ae-incompatible-release-tags) The symbol "dispatch" is marked as @public, but its signature references "TransitionDescription" which is marked as @internal + // + // (undocumented) + dispatch: (action: TransitionDescription) => void; + // (undocumented) + getState: () => State; + // (undocumented) + reducer: Reducer; + // (undocumented) + replaceReducer: (nextReducer: Reducer) => void; + // (undocumented) + subscribe: (listener: (state: State) => void) => () => void; +} + +// @public (undocumented) +export type Selector = (...args: Args) => Result; + +// @public +export interface StateContainer extends BaseStateContainer { + // (undocumented) + selectors: Readonly>; + // Warning: (ae-incompatible-release-tags) The symbol "transitions" is marked as @public, but its signature references "PureTransitionsToTransitions" which is marked as @internal + // + // (undocumented) + transitions: Readonly>; +} + +// @internal (undocumented) +export type Transition = (...args: Args) => State; + +// @internal (undocumented) +export interface TransitionDescription { + // (undocumented) + args: Args; + // (undocumented) + type: Type; +} + +// @public +export type UnboxState> = Container extends StateContainer ? T : never; + +// @public +export const useContainerSelector: , Result>(container: Container, selector: (state: UnboxState) => Result, comparator?: Comparator) => Result; + +// @public +export const useContainerState: >(container: Container) => UnboxState; + + +``` diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts index 69e204a642f93..6bb6e66616c91 100644 --- a/src/plugins/kibana_utils/common/state_containers/create_state_container.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts @@ -44,29 +44,57 @@ const defaultFreeze: (value: T) => T = isProduction return value as T; }; +/** + * State container options + * @public + */ export interface CreateStateContainerOptions { /** - * Function to use when freezing state. Supply identity function + * Function to use when freezing state. Supply identity function. + * If not provided, default `deepFreeze` is used. * + * @example + * If you expect that your state will be mutated externally an you cannot + * prevent that * ```ts * { * freeze: state => state, * } * ``` - * - * if you expect that your state will be mutated externally an you cannot - * prevent that. */ freeze?: (state: T) => T; } +/** + * Creates a state container without transitions and without selectors. + * @param defaultState - initial state + * @typeParam State - shape of state + * @public + */ export function createStateContainer( defaultState: State ): ReduxLikeStateContainer; +/** + * Creates a state container with transitions, but without selectors. + * @param defaultState - initial state + * @param pureTransitions - state transitions configuration object. Map of {@link PureTransition}. + * @typeParam State - shape of state + * @public + */ export function createStateContainer( defaultState: State, pureTransitions: PureTransitions ): ReduxLikeStateContainer; + +/** + * Creates a state container with transitions and selectors. + * @param defaultState - initial state + * @param pureTransitions - state transitions configuration object. Map of {@link PureTransition}. + * @param pureSelectors - state selectors configuration object. Map of {@link PureSelectors}. + * @param options - state container options {@link CreateStateContainerOptions} + * @typeParam State - shape of state + * @public + */ export function createStateContainer< State extends BaseState, PureTransitions extends object, @@ -77,14 +105,17 @@ export function createStateContainer< pureSelectors: PureSelectors, options?: CreateStateContainerOptions ): ReduxLikeStateContainer; +/** + * @internal + */ export function createStateContainer< State extends BaseState, PureTransitions extends object, PureSelectors extends object >( defaultState: State, - pureTransitions: PureTransitions = {} as PureTransitions, - pureSelectors: PureSelectors = {} as PureSelectors, + pureTransitions: PureTransitions = {} as PureTransitions, // TODO: https://github.com/elastic/kibana/issues/54439 + pureSelectors: PureSelectors = {} as PureSelectors, // TODO: https://github.com/elastic/kibana/issues/54439 options: CreateStateContainerOptions = {} ): ReduxLikeStateContainer { const { freeze = defaultFreeze } = options; diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts index 8536f97e00ed0..4712c2fc233f8 100644 --- a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts @@ -17,7 +17,7 @@ * under the License. */ -import * as React from 'react'; +import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import defaultComparator from 'fast-deep-equal'; import { Comparator, Connect, StateContainer, UnboxState } from './types'; @@ -25,23 +25,27 @@ import { Comparator, Connect, StateContainer, UnboxState } from './types'; const { useContext, useLayoutEffect, useRef, createElement: h } = React; /** - * Returns the latest state of a state container. + * React hooks that returns the latest state of a {@link StateContainer}. * - * @param container State container which state to track. + * @param container - {@link StateContainer} which state to track. + * @returns - latest {@link StateContainer} state + * @public */ export const useContainerState = >( container: Container ): UnboxState => useObservable(container.state$, container.get()); /** - * Apply selector to state container to extract only needed information. Will + * React hook to apply selector to state container to extract only needed information. Will * re-render your component only when the section changes. * - * @param container State container which state to track. - * @param selector Function used to pick parts of state. - * @param comparator Comparator function used to memoize previous result, to not + * @param container - {@link StateContainer} which state to track. + * @param selector - Function used to pick parts of state. + * @param comparator - {@link Comparator} function used to memoize previous result, to not * re-render React component if state did not change. By default uses * `fast-deep-equal` package. + * @returns - result of a selector(state) + * @public */ export const useContainerSelector = , Result>( container: Container, @@ -68,6 +72,11 @@ export const useContainerSelector = , return value; }; +/** + * Creates helpers for using {@link StateContainer | State Containers} with react + * Refer to {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_containers/react.md | guide} for details + * @public + */ export const createStateContainerReactHelpers = >() => { const context = React.createContext(null as any); diff --git a/src/plugins/kibana_utils/common/state_containers/index.ts b/src/plugins/kibana_utils/common/state_containers/index.ts index 43e204ecb79f7..e2e056bd67da2 100644 --- a/src/plugins/kibana_utils/common/state_containers/index.ts +++ b/src/plugins/kibana_utils/common/state_containers/index.ts @@ -17,6 +17,40 @@ * under the License. */ -export * from './types'; -export * from './create_state_container'; -export * from './create_state_container_react_helpers'; +/** + * State containers are Redux-store-like objects meant to help you manage state in your services or apps. + * Refer to {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers | guides and examples} for more info + * + * @packageDocumentation + */ + +export { + BaseState, + BaseStateContainer, + TransitionDescription, + StateContainer, + ReduxLikeStateContainer, + Dispatch, + Middleware, + Selector, + Comparator, + MapStateToProps, + Connect, + Reducer, + UnboxState, + PureSelectorToSelector, + PureSelectorsToSelectors, + EnsurePureSelector, + PureTransitionsToTransitions, + PureTransitionToTransition, + EnsurePureTransition, + PureSelector, + PureTransition, + Transition, +} from './types'; +export { createStateContainer, CreateStateContainerOptions } from './create_state_container'; +export { + createStateContainerReactHelpers, + useContainerSelector, + useContainerState, +} from './create_state_container_react_helpers'; diff --git a/src/plugins/kibana_utils/common/state_containers/types.ts b/src/plugins/kibana_utils/common/state_containers/types.ts index 29ffa4cd486b5..b6adb89d9be7b 100644 --- a/src/plugins/kibana_utils/common/state_containers/types.ts +++ b/src/plugins/kibana_utils/common/state_containers/types.ts @@ -19,28 +19,76 @@ import { Observable } from 'rxjs'; import { Ensure } from '@kbn/utility-types'; +import { FC, ComponentType } from 'react'; +/** + * Base {@link StateContainer} state shape + * @public + */ export type BaseState = object; + +/** + * @internal + */ export interface TransitionDescription { type: Type; args: Args; } +/** + * @internal + */ export type Transition = (...args: Args) => State; +/** + * @internal + */ export type PureTransition = ( state: State ) => Transition; +/** + * @public + */ export type EnsurePureTransition = Ensure>; +/** + * @internal + */ export type PureTransitionToTransition> = ReturnType; +/** + * @internal + */ export type PureTransitionsToTransitions = { [K in keyof T]: PureTransitionToTransition>; }; +/** + * Base state container shape without transitions or selectors + * @typeParam State - Shape of state in the container. Have to match {@link BaseState} constraint + * @public + */ export interface BaseStateContainer { + /** + * Retrieves current state from the container + * @returns current state + * @public + */ get: () => State; + /** + * Sets state into container + * @param state - new state to set + */ set: (state: State) => void; + /** + * {@link Observable} of state + */ state$: Observable; } +/** + * Fully featured state container with {@link Selector | Selectors} and {@link Transition | Transitions}. Extends {@link BaseStateContainer}. + * @typeParam State - Shape of state in the container. Has to match {@link BaseState} constraint + * @typeParam PureTransitions - map of {@link PureTransition | transitions} to provide on state container + * @typeParam PureSelectors - map of {@link PureSelector | selectors} to provide on state container + * @public + */ export interface StateContainer< State extends BaseState, PureTransitions extends object = object, @@ -50,6 +98,11 @@ export interface StateContainer< selectors: Readonly>; } +/** + * Fully featured state container which matches Redux store interface. Extends {@link StateContainer}. + * Allows to use state container with redux libraries. + * @public + */ export interface ReduxLikeStateContainer< State extends BaseState, PureTransitions extends object = {}, @@ -63,45 +116,92 @@ export interface ReduxLikeStateContainer< subscribe: (listener: (state: State) => void) => () => void; } +/** + * Redux like dispatch + * @public + */ export type Dispatch = (action: T) => void; +/** + * Redux like Middleware + * @public + */ export type Middleware = ( store: Pick, 'getState' | 'dispatch'> ) => ( next: (action: TransitionDescription) => TransitionDescription | any ) => Dispatch; - +/** + * Redux like Reducer + * @public + */ export type Reducer = ( state: State, action: TransitionDescription ) => State; +/** + * Utility type for inferring state shape from {@link StateContainer} + * @public + */ export type UnboxState< Container extends StateContainer > = Container extends StateContainer ? T : never; +/** + * Utility type for inferring transitions type from {@link StateContainer} + * @public + */ export type UnboxTransitions< Container extends StateContainer > = Container extends StateContainer ? T : never; +/** + * @public + */ export type Selector = (...args: Args) => Result; +/** + * @public + */ export type PureSelector = ( state: State ) => Selector; +/** + * @public + */ export type EnsurePureSelector = Ensure>; +/** + * @public + */ export type PureSelectorToSelector> = ReturnType< EnsurePureSelector >; +/** + * @public + */ export type PureSelectorsToSelectors = { [K in keyof T]: PureSelectorToSelector>; }; +/** + * Used to compare state, see {@link useContainerSelector}. + * @public + */ export type Comparator = (previous: Result, current: Result) => boolean; - +/** + * State container state to component props mapper. + * See {@link Connect} + * @public + */ export type MapStateToProps = ( state: State ) => StateProps; +/** + * Similar to `connect` from react-redux, + * allows to map state from state container to component's props. + * @public + */ export type Connect = < Props extends object, StatePropKeys extends keyof Props >( mapStateToProp: MapStateToProps> -) => (component: React.ComponentType) => React.FC>; +) => (component: ComponentType) => FC>; diff --git a/src/plugins/kibana_utils/public/state_sync/index.ts b/src/plugins/kibana_utils/public/state_sync/index.ts index 1dfa998c5bb9d..da60b36255367 100644 --- a/src/plugins/kibana_utils/public/state_sync/index.ts +++ b/src/plugins/kibana_utils/public/state_sync/index.ts @@ -17,11 +17,32 @@ * under the License. */ +/** + * State syncing utilities are a set of helpers for syncing your application state + * with browser URL or browser storage. + * + * They are designed to work together with {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers | state containers}. But state containers are not required. + * + * State syncing utilities include: + * + * *{@link syncState} util which: + * * Subscribes to state changes and pushes them to state storage. + * * Optionally subscribes to state storage changes and pushes them to state. + * * Two types of storages compatible with `syncState`: + * * {@link IKbnUrlStateStorage} - Serializes state and persists it to URL's query param in rison or hashed format. + * Listens for state updates in the URL and pushes them back to state. + * * {@link ISessionStorageStateStorage} - Serializes state and persists it to browser storage. + * + * Refer {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync | here} for a complete guide and examples. + * @packageDocumentation + */ + export { createSessionStorageStateStorage, createKbnUrlStateStorage, IKbnUrlStateStorage, ISessionStorageStateStorage, + IStateStorage, } from './state_sync_state_storage'; export { IStateSyncConfig, INullableBaseStateContainer } from './types'; export { diff --git a/src/plugins/kibana_utils/public/state_sync/public.api.md b/src/plugins/kibana_utils/public/state_sync/public.api.md new file mode 100644 index 0000000000000..c174ba798d01a --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/public.api.md @@ -0,0 +1,97 @@ +## API Report File for "kibana" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { History } from 'history'; +import { Observable } from 'rxjs'; + +// @public +export const createKbnUrlStateStorage: ({ useHash, history }?: { + useHash: boolean; + history?: History | undefined; +}) => IKbnUrlStateStorage; + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Storage" +// +// @public +export const createSessionStorageStateStorage: (storage?: Storage) => ISessionStorageStateStorage; + +// @public +export interface IKbnUrlStateStorage extends IStateStorage { + cancel: () => void; + // (undocumented) + change$: (key: string) => Observable; + flush: (opts?: { + replace?: boolean; + }) => boolean; + // (undocumented) + get: (key: string) => State | null; + // (undocumented) + set: (key: string, state: State, opts?: { + replace: boolean; + }) => Promise; +} + +// Warning: (ae-forgotten-export) The symbol "BaseState" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "BaseStateContainer" 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 "BaseStateContainer" +// +// @public +export interface INullableBaseStateContainer extends BaseStateContainer { + // (undocumented) + set: (state: State | null) => void; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Storage" +// +// @public +export interface ISessionStorageStateStorage extends IStateStorage { + // (undocumented) + get: (key: string) => State | null; + // (undocumented) + set: (key: string, state: State) => void; +} + +// @public +export interface IStateStorage { + cancel?: () => void; + change$?: (key: string) => Observable; + get: (key: string) => State | null; + set: (key: string, state: State) => any; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "stateSync" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BaseState" +// +// @public +export interface IStateSyncConfig { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BaseStateContainer" + stateContainer: INullableBaseStateContainer; + stateStorage: StateStorage; + storageKey: string; +} + +// @public (undocumented) +export interface ISyncStateRef { + start: StartSyncStateFnType; + stop: StopSyncStateFnType; +} + +// @public (undocumented) +export type StartSyncStateFnType = () => void; + +// @public (undocumented) +export type StopSyncStateFnType = () => void; + +// @public +export function syncState({ storageKey, stateStorage, stateContainer, }: IStateSyncConfig): ISyncStateRef; + +// Warning: (ae-missing-release-tag) "syncStates" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function syncStates(stateSyncConfigs: Array>): ISyncStateRef; + + +``` diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts index 4c400d47b8e78..bbcaaedd0d8bf 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -26,29 +26,61 @@ import { distinctUntilChangedWithInitialValue } from '../../common'; import { BaseState } from '../../common/state_containers'; import { applyDiff } from '../state_management/utils/diff_object'; +/** + * @public + */ +export type StopSyncStateFnType = () => void; +/** + * @public + */ +export type StartSyncStateFnType = () => void; + +/** + * @public + */ +export interface ISyncStateRef { + /** + * stop state syncing + */ + stop: StopSyncStateFnType; + /** + * start state syncing + */ + start: StartSyncStateFnType; +} + /** * Utility for syncing application state wrapped in state container * with some kind of storage (e.g. URL) * - * Examples: + * Go {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync | here} for a complete guide and examples. * - * 1. the simplest use case + * @example + * + * the simplest use case + * ```ts * const stateStorage = createKbnUrlStateStorage(); * syncState({ * storageKey: '_s', * stateContainer, * stateStorage * }); + * ``` * - * 2. conditionally configuring sync strategy + * @example + * conditionally configuring sync strategy + * ```ts * const stateStorage = createKbnUrlStateStorage({useHash: config.get('state:stateContainerInSessionStorage')}) * syncState({ * storageKey: '_s', * stateContainer, * stateStorage * }); + * ``` * - * 3. implementing custom sync strategy + * @example + * implementing custom sync strategy + * ```ts * const localStorageStateStorage = { * set: (storageKey, state) => localStorage.setItem(storageKey, JSON.stringify(state)), * get: (storageKey) => localStorage.getItem(storageKey) ? JSON.parse(localStorage.getItem(storageKey)) : null @@ -58,12 +90,15 @@ import { applyDiff } from '../state_management/utils/diff_object'; * stateContainer, * stateStorage: localStorageStateStorage * }); + * ``` * - * 4. Transform state before serialising + * @example + * transforming state before serialising * Useful for: * * Migration / backward compatibility * * Syncing part of state * * Providing default values + * ```ts * const stateToStorage = (s) => ({ tab: s.tab }); * syncState({ * storageKey: '_s', @@ -74,20 +109,12 @@ import { applyDiff } from '../state_management/utils/diff_object'; * }, * stateStorage * }); + * ``` * - * Caveats: - * - * 1. It is responsibility of consumer to make sure that initial app state and storage are in sync before starting syncing - * No initial sync happens when syncState() is called + * @param - syncing config {@link IStateSyncConfig} + * @returns - {@link ISyncStateRef} + * @public */ -export type StopSyncStateFnType = () => void; -export type StartSyncStateFnType = () => void; -export interface ISyncStateRef { - // stop syncing state with storage - stop: StopSyncStateFnType; - // start syncing state with storage - start: StartSyncStateFnType; -} export function syncState< State extends BaseState, StateStorage extends IStateStorage = IStateStorage @@ -159,7 +186,9 @@ export function syncState< } /** - * multiple different sync configs + * @example + * sync multiple different sync configs + * ```ts * syncStates([ * { * storageKey: '_s1', @@ -172,7 +201,8 @@ export function syncState< * stateContainer: stateContainer2, * }, * ]); - * @param stateSyncConfigs - Array of IStateSyncConfig to sync + * ``` + * @param stateSyncConfigs - Array of {@link IStateSyncConfig} to sync */ export function syncStates(stateSyncConfigs: Array>): ISyncStateRef { const syncRefs = stateSyncConfigs.map((config) => syncState(config)); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts index 67c1bf26aa251..0c74e1eb9f421 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -27,6 +27,19 @@ import { setStateToKbnUrl, } from '../../state_management/url'; +/** + * KbnUrlStateStorage is a state storage for {@link syncState} utility which: + * + * 1. Keeps state in sync with the URL. + * 2. Serializes data and stores it in the URL in one of the supported formats: + * * Rison encoded. + * * Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See Kibana's `state:storeInSessionStorage` advanced option for more context. + * 3. Takes care of listening to the URL updates and notifies state about the updates. + * 4. Takes care of batching URL updates to prevent redundant browser history records. + * + * {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md | Refer to this guide for more info} + * @public + */ export interface IKbnUrlStateStorage extends IStateStorage { set: ( key: string, @@ -36,18 +49,23 @@ export interface IKbnUrlStateStorage extends IStateStorage { get: (key: string) => State | null; change$: (key: string) => Observable; - // cancels any pending url updates + /** + * cancels any pending url updates + */ cancel: () => void; - // synchronously runs any pending url updates - // returned boolean indicates if change occurred + /** + * Synchronously runs any pending url updates, returned boolean indicates if change occurred. + * @param opts: {replace? boolean} - allows to specify if push or replace should be used for flushing update + * @returns boolean - indicates if there was an update to flush + */ flush: (opts?: { replace?: boolean }) => boolean; } /** - * Implements syncing to/from url strategies. - * Replicates what was implemented in state (AppState, GlobalState) - * Both expanded and hashed use cases + * Creates {@link IKbnUrlStateStorage} state storage + * @returns - {@link IKbnUrlStateStorage} + * @public */ export const createKbnUrlStateStorage = ( { useHash = false, history }: { useHash: boolean; history?: History } = { useHash: false } diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts index 00edfdfd1ed61..60ff211cd590a 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts @@ -19,11 +19,23 @@ import { IStateStorage } from './types'; +/** + * {@link IStateStorage} for storing state in browser {@link Storage} + * {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md | guide} + * @public + */ export interface ISessionStorageStateStorage extends IStateStorage { set: (key: string, state: State) => void; get: (key: string) => State | null; } +/** + * Creates {@link ISessionStorageStateStorage} + * {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md | guide} + * @param storage - Option {@link Storage} to use for storing state. By default window.sessionStorage. + * @returns - {@link ISessionStorageStateStorage} + * @public + */ export const createSessionStorageStateStorage = ( storage: Storage = window.sessionStorage ): ISessionStorageStateStorage => { diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts index add1dc259be45..bae5dc2067183 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts @@ -25,7 +25,8 @@ import { Observable } from 'rxjs'; * * state serialisation / deserialization * * persisting to and retrieving from storage * - * For an example take a look at already implemented KbnUrl state storage + * For an example take a look at already implemented {@link IKbnUrlStateStorage} and {@link ISessionStorageStateStorage} state storages + * @public */ export interface IStateStorage { /** @@ -45,7 +46,7 @@ export interface IStateStorage { /** * Optional method to cancel any pending activity - * syncState() will call it, if it is provided by IStateStorage + * {@link syncState} will call it during destroy, if it is provided by IStateStorage */ cancel?: () => void; } diff --git a/src/plugins/kibana_utils/public/state_sync/types.ts b/src/plugins/kibana_utils/public/state_sync/types.ts index 2acb466d92e92..e879ab7c55b2f 100644 --- a/src/plugins/kibana_utils/public/state_sync/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/types.ts @@ -20,15 +20,26 @@ import { BaseState, BaseStateContainer } from '../../common/state_containers/types'; import { IStateStorage } from './state_sync_state_storage'; +/** + * Extension of {@link BaseStateContainer} with one constraint: set state should handle `null` as incoming state + * @remarks + * State container for `stateSync()` have to accept `null` + * for example, `set()` implementation could handle null and fallback to some default state + * this is required to handle edge case, when state in storage becomes empty and syncing is in progress. + * State container will be notified about about storage becoming empty with null passed in. + * @public + */ export interface INullableBaseStateContainer extends BaseStateContainer { - // State container for stateSync() have to accept "null" - // for example, set() implementation could handle null and fallback to some default state - // this is required to handle edge case, when state in storage becomes empty and syncing is in progress. - // state container will be notified about about storage becoming empty with null passed in set: (state: State | null) => void; } +/** + * Config for setting up state syncing with {@link stateSync} + * @typeParam State - State shape to sync to storage, has to extend {@link BaseState} + * @typeParam StateStorage - used state storage to sync state with + * @public + */ export interface IStateSyncConfig< State extends BaseState, StateStorage extends IStateStorage = IStateStorage @@ -39,8 +50,8 @@ export interface IStateSyncConfig< */ storageKey: string; /** - * State container to keep in sync with storage, have to implement INullableBaseStateContainer interface - * The idea is that ./state_containers/ should be used as a state container, + * State container to keep in sync with storage, have to implement {@link INullableBaseStateContainer} interface + * We encourage to use {@link BaseStateContainer} as a state container, * but it is also possible to implement own custom container for advanced use cases */ stateContainer: INullableBaseStateContainer; @@ -49,7 +60,7 @@ export interface IStateSyncConfig< * State storage is responsible for serialising / deserialising and persisting / retrieving stored state * * There are common strategies already implemented: - * './state_sync_state_storage/' + * see {@link IKbnUrlStateStorage} * which replicate what State (AppState, GlobalState) in legacy world did * */ diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index e555c40d25592..6c8888feafc1f 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -75,7 +75,7 @@ export class TelemetryPlugin implements Plugin { } public async setup( - { elasticsearch, http, savedObjects, metrics }: CoreSetup, + { elasticsearch, http, savedObjects }: CoreSetup, { usageCollection, telemetryCollectionManager }: TelemetryPluginsSetup ) { const currentKibanaVersion = this.currentKibanaVersion; diff --git a/src/plugins/testbed/kibana.json b/src/plugins/testbed/kibana.json deleted file mode 100644 index 9afe357b7a010..0000000000000 --- a/src/plugins/testbed/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "testbed", - "version": "0.0.1", - "kibanaVersion": "kibana", - "configPath": ["core", "testbed"], - "server": true, - "ui": true -} diff --git a/src/plugins/testbed/public/index.ts b/src/plugins/testbed/public/index.ts deleted file mode 100644 index 601db10f6f8bb..0000000000000 --- a/src/plugins/testbed/public/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; -import { TestbedPlugin, TestbedPluginSetup, TestbedPluginStart } from './plugin'; - -export const plugin: PluginInitializer = ( - initializerContext: PluginInitializerContext -) => new TestbedPlugin(initializerContext); diff --git a/src/plugins/testbed/public/plugin.ts b/src/plugins/testbed/public/plugin.ts deleted file mode 100644 index 8c70485d9ee8b..0000000000000 --- a/src/plugins/testbed/public/plugin.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/public'; - -interface ConfigType { - uiProp: string; -} - -export class TestbedPlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - - public async setup(core: CoreSetup, deps: {}) { - const config = this.initializerContext.config.get(); - - // eslint-disable-next-line no-console - console.log(`Testbed plugin set up. uiProp: '${config.uiProp}'`); - return { - foo: 'bar', - }; - } - - public start() { - // eslint-disable-next-line no-console - console.log(`Testbed plugin started`); - } - - public stop() {} -} - -export type TestbedPluginSetup = ReturnType; -export type TestbedPluginStart = ReturnType; diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts deleted file mode 100644 index 21f97259c97f4..0000000000000 --- a/src/plugins/testbed/server/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { map } from 'rxjs/operators'; -import { schema, TypeOf } from '@kbn/config-schema'; - -import { - CoreSetup, - CoreStart, - Logger, - PluginInitializerContext, - PluginConfigDescriptor, - PluginName, -} from 'kibana/server'; - -const configSchema = schema.object({ - secret: schema.string({ defaultValue: 'Not really a secret :/' }), - uiProp: schema.string({ defaultValue: 'Accessible from client' }), -}); - -type ConfigType = TypeOf; - -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - uiProp: true, - }, - schema: configSchema, - deprecations: ({ rename, unused, renameFromRoot }) => [ - rename('securityKey', 'secret'), - renameFromRoot('oldtestbed.uiProp', 'testbed.uiProp'), - unused('deprecatedProperty'), - ], -}; - -class Plugin { - private readonly log: Logger; - - constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get(); - } - - public setup(core: CoreSetup, deps: Record) { - this.log.debug( - `Setting up TestBed with core contract [${Object.keys(core)}] and deps [${Object.keys(deps)}]` - ); - - const router = core.http.createRouter(); - router.get( - { path: '/requestcontext/elasticsearch', validate: false }, - async (context, req, res) => { - const response = await context.core.elasticsearch.legacy.client.callAsInternalUser('ping'); - return res.ok({ body: `Elasticsearch: ${response}` }); - } - ); - - router.get( - { path: '/requestcontext/savedobjectsclient', validate: false }, - async (context, req, res) => { - const response = await context.core.savedObjects.client.find({ type: 'TYPE' }); - return res.ok({ body: `SavedObjects client: ${JSON.stringify(response)}` }); - } - ); - - return { - data$: this.initializerContext.config.create().pipe( - map((configValue) => { - this.log.debug(`I've got value from my config: ${configValue.secret}`); - return `Some exposed data derived from config: ${configValue.secret}`; - }) - ), - pingElasticsearch: async () => { - const [coreStart] = await core.getStartServices(); - return coreStart.elasticsearch.legacy.client.callAsInternalUser('ping'); - }, - }; - } - - public start(core: CoreStart, deps: Record) { - this.log.debug( - `Starting up TestBed testbed with core contract [${Object.keys( - core - )}] and deps [${Object.keys(deps)}]` - ); - - return { - getStartContext() { - return core; - }, - }; - } - - public stop() { - this.log.debug(`Stopping TestBed`); - } -} - -export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx index ca8750a991d83..7c930fa2e2960 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx @@ -22,6 +22,8 @@ import { ColorPicker, ColorPickerProps } from './color_picker'; import { mount } from 'enzyme'; import { ReactWrapper } from 'enzyme'; import { EuiColorPicker, EuiIconTip } from '@elastic/eui'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; describe('ColorPicker', () => { const defaultProps: ColorPickerProps = { @@ -42,6 +44,22 @@ describe('ColorPicker', () => { expect(component.find('.tvbColorPicker__clear').length).toBe(0); }); + it('should render the correct value to the input text if the prop value is hex', () => { + const props = { ...defaultProps, value: '#68BC00' }; + component = mount(); + component.find('.tvbColorPicker button').simulate('click'); + const input = findTestSubject(component, 'topColorPickerInput'); + expect(input.props().value).toBe('#68BC00'); + }); + + it('should render the correct value to the input text if the prop value is rgba', () => { + const props = { ...defaultProps, value: 'rgba(85,66,177,1)' }; + component = mount(); + component.find('.tvbColorPicker button').simulate('click'); + const input = findTestSubject(component, 'topColorPickerInput'); + expect(input.props().value).toBe('85,66,177,1'); + }); + it('should render the correct aria label to the color swatch button', () => { const props = { ...defaultProps, value: 'rgba(85,66,177,0.59)' }; component = mount(); diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx index be580c80d5941..444e5c90c7a6d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx @@ -43,8 +43,10 @@ export interface ColorPickerProps { } export function ColorPicker({ name, value, disableTrash = false, onChange }: ColorPickerProps) { - const initialColorValue = value ? value.replace(COMMAS_NUMS_ONLY_RE, '') : ''; - const [color, setColor] = useState(initialColorValue); + const initialColorValue = value?.includes('rgba') + ? value.replace(COMMAS_NUMS_ONLY_RE, '') + : value; + const [color, setColor] = useState(initialColorValue || ''); const handleColorChange: EuiColorPickerProps['onChange'] = (text: string, { rgba, hex }) => { setColor(text); diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx index dd89e98fb8fe5..f48febfef5b43 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx @@ -165,7 +165,7 @@ describe('NewVisModal', () => { ); const visButton = wrapper.find('button[data-test-subj="visType-visWithAliasUrl"]'); visButton.simulate('click'); - expect(stateTransfer.navigateToWithOriginatingApp).toBeCalledWith('otherApp', { + expect(stateTransfer.navigateToEditor).toBeCalledWith('otherApp', { path: '#/aliasUrl', state: { originatingApp: 'coolJestTestApp' }, }); diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index 84a5bca0ed0ed..1d01900ceffc2 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -172,7 +172,7 @@ class NewVisModal extends React.Component originatingApp; const visStateToEditorState = () => { diff --git a/test/api_integration/apis/core/index.js b/test/api_integration/apis/core/index.js index c522acaea25a3..ab9bb8d33c2dc 100644 --- a/test/api_integration/apis/core/index.js +++ b/test/api_integration/apis/core/index.js @@ -22,19 +22,6 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('core', () => { - describe('request context', () => { - it('provides access to elasticsearch', async () => - await supertest.get('/requestcontext/elasticsearch').expect(200, 'Elasticsearch: true')); - - it('provides access to SavedObjects client', async () => - await supertest - .get('/requestcontext/savedobjectsclient') - .expect( - 200, - 'SavedObjects client: {"page":1,"per_page":20,"total":0,"saved_objects":[]}' - )); - }); - describe('compression', () => { it(`uses compression when there isn't a referer`, async () => { await supertest diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index ecaa5aa2da97f..de9606f3d02ed 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -257,5 +257,16 @@ export default function ({ getService, getPageObjects }) { expect(refreshedTimeString).not.to.be(initialTimeString); }); }); + + describe('invalid time range in URL', function () { + it('should display a "Invalid time range toast"', async function () { + await PageObjects.common.navigateToUrl('discover', '#/?_g=(time:(from:now-15m,to:null))', { + useActualUrl: true, + }); + await PageObjects.header.awaitKibanaChrome(); + const toastMessage = await PageObjects.common.closeToast(); + expect(toastMessage).to.be('Invalid time range'); + }); + }); }); } diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 236b2fb9f2f1e..8c5a99204bab6 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -399,7 +399,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo const toast = await find.byCssSelector('.euiToast', 2 * defaultFindTimeout); await toast.moveMouseTo(); const title = await (await find.byCssSelector('.euiToastHeader__title')).getVisibleText(); - log.debug(`Toast title: ${title}`); + await find.clickByCssSelector('.euiToast__closeButton'); return title; } diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index c9a5dcfba32b1..0f5d6ea74a6b6 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -213,5 +213,19 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await testSubjects.click('saveNewTitleButton'); await this.toggleContextMenu(panel); } + + async getActionWebElementByText(text: string): Promise { + log.debug(`getActionWebElement: "${text}"`); + const menu = await testSubjects.find('multipleActionsContextMenu'); + const items = await menu.findAllByCssSelector('[data-test-subj*="embeddablePanelAction-"]'); + for (const item of items) { + const currentText = await item.getVisibleText(); + if (currentText === text) { + return item; + } + } + + throw new Error(`No action matching text "${text}"`); + } })(); } diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts index 66f32d246b31f..a25695a5bfcb7 100644 --- a/test/functional/services/visualizations/pie_chart.ts +++ b/test/functional/services/visualizations/pie_chart.ts @@ -28,10 +28,13 @@ export function PieChartProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const defaultFindTimeout = config.get('timeouts.find'); + const panelActions = getService('dashboardPanelActions'); return new (class PieChart { - async filterOnPieSlice(name?: string) { - log.debug(`PieChart.filterOnPieSlice(${name})`); + private readonly filterActionText = 'Apply filter to current view'; + + async clickOnPieSlice(name?: string) { + log.debug(`PieChart.clickOnPieSlice(${name})`); if (name) { await testSubjects.click(`pieSlice-${name.split(' ').join('-')}`); } else { @@ -44,6 +47,16 @@ export function PieChartProvider({ getService }: FtrProviderContext) { } } + async filterOnPieSlice(name?: string) { + log.debug(`PieChart.filterOnPieSlice(${name})`); + await this.clickOnPieSlice(name); + const hasUiActionsPopup = await testSubjects.exists('multipleActionsContextMenu'); + if (hasUiActionsPopup) { + const actionElement = await panelActions.getActionWebElementByText(this.filterActionText); + await actionElement.click(); + } + } + async filterByLegendItem(label: string) { log.debug(`PieChart.filterByLegendItem(${label})`); await testSubjects.click(`legend-${label}`); diff --git a/x-pack/mocks.ts b/x-pack/mocks.ts index 28c589bee4baa..777c8d0a08131 100644 --- a/x-pack/mocks.ts +++ b/x-pack/mocks.ts @@ -9,7 +9,7 @@ import { licensingMock } from './plugins/licensing/server/mocks'; function createCoreRequestHandlerContextMock() { return { core: coreMock.createRequestHandlerContext(), - licensing: { license: licensingMock.createLicense() }, + licensing: licensingMock.createRequestHandlerContext(), }; } diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index c98d9bcbd9ae5..06ce8d673e6b7 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -6,6 +6,7 @@ import { SavedObjectsServiceSetup } from 'kibana/server'; import mappings from './mappings.json'; +import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; export function setupSavedObjects( @@ -16,6 +17,7 @@ export function setupSavedObjects( name: 'alert', hidden: true, namespaceType: 'single', + migrations: getMigrations(encryptedSavedObjects), mappings: mappings.alert, }); diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..38cda5a9a0f7c --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { getMigrations } from './migrations'; +import { RawAlert } from '../types'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; +import { migrationMocks } from 'src/core/server/mocks'; + +const { log } = migrationMocks.createContext(); +const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + +describe('7.9.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('changes nothing on alerts by other plugins', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const alert = getMockData({}); + expect(migration790(alert, { log })).toMatchObject(alert); + + expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function) + ); + }); + + test('migrates the consumer for alerting', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const alert = getMockData({ + consumer: 'alerting', + }); + expect(migration790(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + consumer: 'alerts', + }, + }); + }); +}); + +function getMockData( + overwrites: Record = {} +): SavedObjectUnsanitizedDoc { + return { + attributes: { + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: '123', + consumer: 'bar', + apiKey: '', + apiKeyOwner: '', + schedule: { interval: '10s' }, + throttle: null, + params: { + bar: true, + }, + muteAll: false, + mutedInstanceIds: [], + createdBy: new Date().toISOString(), + updatedBy: new Date().toISOString(), + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: '1', + actionTypeId: '1', + params: { + foo: true, + }, + }, + ], + ...overwrites, + }, + id: uuid.v4(), + type: 'alert', + }; +} diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..142102dd711c7 --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + SavedObjectMigrationMap, + SavedObjectUnsanitizedDoc, + SavedObjectMigrationFn, +} from '../../../../../src/core/server'; +import { RawAlert } from '../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function getMigrations( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +): SavedObjectMigrationMap { + return { + '7.9.0': changeAlertingConsumer(encryptedSavedObjects), + }; +} + +/** + * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts` + * prior to that we were using `alerting` and we need to keep these in sync + */ +function changeAlertingConsumer( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +): SavedObjectMigrationFn { + const consumerMigration = new Map(); + consumerMigration.set('alerting', 'alerts'); + + return encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return consumerMigration.has(doc.attributes.consumer); + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + const { + attributes: { consumer }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + consumer: consumerMigration.get(consumer) ?? consumer, + }, + }; + } + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index cb6003c58e90d..cdebb3aac129b 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -12,11 +12,19 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { PROJECTION } from '../../../../common/projections/typings'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; + +type TracesAPIResponse = APIReturnType<'/api/apm/traces'>; +const DEFAULT_RESPONSE: TracesAPIResponse = { + items: [], + isAggregationAccurate: true, + bucketSize: 0, +}; export function TraceOverview() { const { urlParams, uiFilters } = useUrlParams(); const { start, end } = urlParams; - const { status, data = [] } = useFetcher( + const { status, data = DEFAULT_RESPONSE } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -56,7 +64,7 @@ export function TraceOverview() { diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index fc5347d081316..a1e01b61d5c1b 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -11,16 +11,21 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, + EuiCallOut, + EuiCode, } from '@elastic/eui'; import { Location } from 'history'; +import { FormattedMessage } from '@kbn/i18n/react'; import { first } from 'lodash'; import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { useTransactionList } from '../../../hooks/useTransactionList'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; import { TransactionCharts } from '../../shared/charts/TransactionCharts'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { TransactionList } from './List'; +import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { useRedirect } from './useRedirect'; import { history } from '../../../utils/history'; import { useLocation } from '../../../hooks/useLocation'; @@ -140,9 +145,48 @@ export function TransactionOverview() {

Transactions

+ {!transactionListData.isAggregationAccurate && ( + +

+ + xpack.apm.ui.transactionGroupBucketSize + + ), + }} + /> + + + {i18n.translate( + 'xpack.apm.transactionCardinalityWarning.docsLink', + { defaultMessage: 'Learn more in the docs' } + )} + +

+
+ )} +
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts index 202437ae72257..ed6bb9309a557 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts @@ -8,8 +8,7 @@ import { useMemo } from 'react'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useUiFilters } from '../context/UrlParamsContext'; import { useFetcher } from './useFetcher'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroupListAPIResponse } from '../../server/lib/transaction_groups'; +import { APIReturnType } from '../services/rest/createCallApmApi'; const getRelativeImpact = ( impact: number, @@ -21,7 +20,11 @@ const getRelativeImpact = ( 1 ); -function getWithRelativeImpact(items: TransactionGroupListAPIResponse) { +type TransactionsAPIResponse = APIReturnType< + '/api/apm/services/{serviceName}/transaction_groups' +>; + +function getWithRelativeImpact(items: TransactionsAPIResponse['items']) { const impacts = items .map(({ impact }) => impact) .filter((impact) => impact !== null) as number[]; @@ -40,10 +43,16 @@ function getWithRelativeImpact(items: TransactionGroupListAPIResponse) { }); } +const DEFAULT_RESPONSE: TransactionsAPIResponse = { + items: [], + isAggregationAccurate: true, + bucketSize: 0, +}; + export function useTransactionList(urlParams: IUrlParams) { const { serviceName, transactionType, start, end } = urlParams; const uiFilters = useUiFilters(urlParams); - const { data = [], error, status } = useFetcher( + const { data = DEFAULT_RESPONSE, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && transactionType) { return callApmApi({ @@ -63,7 +72,14 @@ export function useTransactionList(urlParams: IUrlParams) { [serviceName, start, end, transactionType, uiFilters] ); - const memoizedData = useMemo(() => getWithRelativeImpact(data), [data]); + const memoizedData = useMemo( + () => ({ + items: getWithRelativeImpact(data.items), + isAggregationAccurate: data.isAggregationAccurate, + bucketSize: data.bucketSize, + }), + [data] + ); return { data: memoizedData, status, diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index e9de8fcd890d0..0e495391c94f2 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -38,6 +38,11 @@ import { setHelpExtension } from './setHelpExtension'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; import { setReadonlyBadge } from './updateBadge'; import { createStaticIndexPattern } from './services/rest/index_pattern'; +import { + fetchLandingPageData, + hasData, +} from './services/rest/observability_dashboard'; +import { getTheme } from './utils/get_theme'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -73,6 +78,19 @@ export class ApmPlugin implements Plugin { pluginSetupDeps.home.environment.update({ apmUi: true }); pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); + if (plugins.observability) { + const theme = getTheme({ + isDarkMode: core.uiSettings.get('theme:darkMode'), + }); + plugins.observability.dashboard.register({ + appName: 'apm', + fetchData: async (params) => { + return fetchLandingPageData(params, { theme }); + }, + hasData, + }); + } + core.application.register({ id: 'apm', title: 'APM', diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index 44768c94f3b1d..8babc72ef129c 100644 --- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -8,7 +8,7 @@ import { callApi, FetchOptions } from './callApi'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMAPI } from '../../../server/routes/create_apm_api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Client } from '../../../server/routes/typings'; +import { Client, HttpMethod } from '../../../server/routes/typings'; export type APMClient = Client; export type APMClientOptions = Omit & { @@ -43,3 +43,11 @@ export function createCallApmApi(http: HttpSetup) { }); }) as APMClient; } + +// infer return type from API +export type APIReturnType< + TPath extends keyof APMAPI['_S'], + TMethod extends HttpMethod = 'GET' +> = APMAPI['_S'][TPath] extends { [key in TMethod]: { ret: any } } + ? APMAPI['_S'][TPath][TMethod]['ret'] + : unknown; diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts new file mode 100644 index 0000000000000..1ee8d79ee99a5 --- /dev/null +++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fetchLandingPageData, hasData } from './observability_dashboard'; +import * as createCallApmApi from './createCallApmApi'; +import { getTheme } from '../../utils/get_theme'; + +const theme = getTheme({ isDarkMode: false }); + +describe('Observability dashboard data', () => { + const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi'); + afterEach(() => { + callApmApiMock.mockClear(); + }); + describe('hasData', () => { + it('returns false when no data is available', async () => { + callApmApiMock.mockImplementation(() => Promise.resolve(false)); + const response = await hasData(); + expect(response).toBeFalsy(); + }); + it('returns true when data is available', async () => { + callApmApiMock.mockImplementation(() => Promise.resolve(true)); + const response = await hasData(); + expect(response).toBeTruthy(); + }); + }); + + describe('fetchLandingPageData', () => { + it('returns APM data with series and stats', async () => { + callApmApiMock.mockImplementation(() => + Promise.resolve({ + serviceCount: 10, + transactionCoordinates: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ], + }) + ); + const response = await fetchLandingPageData( + { + startTime: '1', + endTime: '2', + bucketSize: '3', + }, + { theme } + ); + expect(response).toEqual({ + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + label: 'Services', + value: 10, + }, + transactions: { + type: 'number', + label: 'Transactions', + value: 6, + color: '#6092c0', + }, + }, + series: { + transactions: { + label: 'Transactions', + coordinates: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ], + color: '#6092c0', + }, + }, + }); + }); + it('returns empty transaction coordinates', async () => { + callApmApiMock.mockImplementation(() => + Promise.resolve({ + serviceCount: 0, + transactionCoordinates: [], + }) + ); + const response = await fetchLandingPageData( + { + startTime: '1', + endTime: '2', + bucketSize: '3', + }, + { theme } + ); + expect(response).toEqual({ + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + label: 'Services', + value: 0, + }, + transactions: { + type: 'number', + label: 'Transactions', + value: 0, + color: '#6092c0', + }, + }, + series: { + transactions: { + label: 'Transactions', + coordinates: [], + color: '#6092c0', + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts new file mode 100644 index 0000000000000..2221904932b63 --- /dev/null +++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { sum } from 'lodash'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FetchDataParams } from '../../../../observability/public/data_handler'; +import { ApmFetchDataResponse } from '../../../../observability/public/typings/fetch_data_response'; +import { callApmApi } from './createCallApmApi'; +import { Theme } from '../../utils/get_theme'; + +interface Options { + theme: Theme; +} + +export const fetchLandingPageData = async ( + { startTime, endTime, bucketSize }: FetchDataParams, + { theme }: Options +): Promise => { + const data = await callApmApi({ + pathname: '/api/apm/observability_dashboard', + params: { query: { start: startTime, end: endTime, bucketSize } }, + }); + + const { serviceCount, transactionCoordinates } = data; + + return { + title: i18n.translate('xpack.apm.observabilityDashboard.title', { + defaultMessage: 'APM', + }), + appLink: '/app/apm', + stats: { + services: { + type: 'number', + label: i18n.translate( + 'xpack.apm.observabilityDashboard.stats.services', + { defaultMessage: 'Services' } + ), + value: serviceCount, + }, + transactions: { + type: 'number', + label: i18n.translate( + 'xpack.apm.observabilityDashboard.stats.transactions', + { defaultMessage: 'Transactions' } + ), + value: sum(transactionCoordinates.map((coordinates) => coordinates.y)), + color: theme.euiColorVis1, + }, + }, + series: { + transactions: { + label: i18n.translate( + 'xpack.apm.observabilityDashboard.chart.transactions', + { defaultMessage: 'Transactions' } + ), + color: theme.euiColorVis1, + coordinates: transactionCoordinates, + }, + }, + }; +}; + +export async function hasData() { + return await callApmApi({ + pathname: '/api/apm/observability_dashboard/has_data', + }); +} diff --git a/x-pack/plugins/apm/public/utils/get_theme.ts b/x-pack/plugins/apm/public/utils/get_theme.ts new file mode 100644 index 0000000000000..e5020202b7721 --- /dev/null +++ b/x-pack/plugins/apm/public/utils/get_theme.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; + +export type Theme = ReturnType; + +export function getTheme({ isDarkMode }: { isDarkMode: boolean }) { + return isDarkMode ? darkTheme : lightTheme; +} diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/get_service_count.ts new file mode 100644 index 0000000000000..4c4d058c7139d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/get_service_count.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ProcessorEvent } from '../../../common/processor_event'; +import { rangeFilter } from '../../../common/utils/range_filter'; +import { + SERVICE_NAME, + PROCESSOR_EVENT, +} from '../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; + +export async function getServiceCount({ + setup, +}: { + setup: Setup & SetupTimeRange; +}) { + const { client, indices, start, end } = setup; + + const params = { + index: [ + indices['apm_oss.transactionIndices'], + indices['apm_oss.errorIndices'], + indices['apm_oss.metricsIndices'], + ], + body: { + size: 0, + query: { + bool: { + filter: [ + { range: rangeFilter(start, end) }, + { + terms: { + [PROCESSOR_EVENT]: [ + ProcessorEvent.error, + ProcessorEvent.transaction, + ProcessorEvent.metric, + ], + }, + }, + ], + }, + }, + aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, + }, + }; + + const { aggregations } = await client.search(params); + return aggregations?.serviceCount.value || 0; +} diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts new file mode 100644 index 0000000000000..78ed11d839ad2 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { rangeFilter } from '../../../common/utils/range_filter'; +import { Coordinates } from '../../../../observability/public/typings/fetch_data_response'; +import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { ProcessorEvent } from '../../../common/processor_event'; + +export async function getTransactionCoordinates({ + setup, + bucketSize, +}: { + setup: Setup & SetupTimeRange; + bucketSize: string; +}): Promise { + const { client, indices, start, end } = setup; + + const { aggregations } = await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { range: rangeFilter(start, end) }, + ], + }, + }, + aggs: { + distribution: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + }, + }, + }, + }); + + return ( + aggregations?.distribution.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.doc_count, + })) || [] + ); +} diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/has_data.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/has_data.ts new file mode 100644 index 0000000000000..73cc2d273ec69 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/has_data.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 { Setup } from '../helpers/setup_request'; + +export async function hasData({ setup }: { setup: Setup }) { + const { client, indices } = setup; + try { + const params = { + index: [ + indices['apm_oss.transactionIndices'], + indices['apm_oss.errorIndices'], + indices['apm_oss.metricsIndices'], + ], + terminateAfter: 1, + size: 0, + }; + + const response = await client.search(params); + return response.hits.total.value > 0; + } catch (e) { + return false; + } +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap index 64f06ad0a81cd..087dc6afc9a58 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap @@ -46,7 +46,7 @@ Array [ }, }, "composite": Object { - "size": 10000, + "size": 101, "sources": Array [ Object { "service": Object { @@ -159,7 +159,7 @@ Array [ }, }, "composite": Object { - "size": 10000, + "size": 101, "sources": Array [ Object { "transaction": Object { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index b93f842b878cb..496533cf97e65 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -44,7 +44,7 @@ Object { }, }, "composite": Object { - "size": 10000, + "size": 101, "sources": Array [ Object { "service": Object { @@ -153,7 +153,7 @@ Object { }, }, "composite": Object { - "size": 10000, + "size": 101, "sources": Array [ Object { "transaction": Object { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts index 00702be6744ec..a26c3d85a3fc4 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts @@ -39,7 +39,8 @@ describe('transactionGroupsFetcher', () => { describe('type: top_traces', () => { it('should call client.search with correct query', async () => { const setup = getSetup(); - await transactionGroupsFetcher({ type: 'top_traces' }, setup); + const bucketSize = 100; + await transactionGroupsFetcher({ type: 'top_traces' }, setup, bucketSize); expect(setup.client.search.mock.calls).toMatchSnapshot(); }); }); @@ -47,13 +48,15 @@ describe('transactionGroupsFetcher', () => { describe('type: top_transactions', () => { it('should call client.search with correct query', async () => { const setup = getSetup(); + const bucketSize = 100; await transactionGroupsFetcher( { type: 'top_transactions', serviceName: 'opbeans-node', transactionType: 'request', }, - setup + setup, + bucketSize ); expect(setup.client.search.mock.calls).toMatchSnapshot(); }); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index d10c45ecbdbfb..595ee9d8da2dc 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -36,9 +36,10 @@ interface TopTraceOptions { export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; -export function transactionGroupsFetcher( +export async function transactionGroupsFetcher( options: Options, - setup: Setup & SetupTimeRange & SetupUIFilters + setup: Setup & SetupTimeRange & SetupUIFilters, + bucketSize: number ) { const { client } = setup; @@ -71,7 +72,7 @@ export function transactionGroupsFetcher( aggs: { transaction_groups: { composite: { - size: 10000, + size: bucketSize + 1, // 1 extra bucket is added to check whether the total number of buckets exceed the specified bucket size. sources: [ ...(isTopTraces ? [{ service: { terms: { field: SERVICE_NAME } } }] diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/index.ts b/x-pack/plugins/apm/server/lib/transaction_groups/index.ts index 30c4975120483..893e586b351a8 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/index.ts @@ -11,20 +11,18 @@ import { } from '../helpers/setup_request'; import { transactionGroupsFetcher, Options } from './fetcher'; import { transactionGroupsTransformer } from './transform'; -import { PromiseReturnType } from '../../../../observability/typings/common'; -export type TransactionGroupListAPIResponse = PromiseReturnType< - typeof getTransactionGroupList ->; export async function getTransactionGroupList( options: Options, setup: Setup & SetupTimeRange & SetupUIFilters ) { const { start, end } = setup; - const response = await transactionGroupsFetcher(options, setup); + const bucketSize = setup.config['xpack.apm.ui.transactionGroupBucketSize']; + const response = await transactionGroupsFetcher(options, setup, bucketSize); return transactionGroupsTransformer({ response, start, end, + bucketSize, }); } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts index 58d770bebce97..2c5aa79bb3483 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -18,6 +18,7 @@ describe('transaction group queries', () => { }); it('fetches top transactions', async () => { + const bucketSize = 100; mock = await inspectSearchParams((setup) => transactionGroupsFetcher( { @@ -25,7 +26,8 @@ describe('transaction group queries', () => { serviceName: 'foo', transactionType: 'bar', }, - setup + setup, + bucketSize ) ); @@ -33,12 +35,14 @@ describe('transaction group queries', () => { }); it('fetches top traces', async () => { + const bucketSize = 100; mock = await inspectSearchParams((setup) => transactionGroupsFetcher( { type: 'top_traces', }, - setup + setup, + bucketSize ) ); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts index e5ec9a8eae782..0bb29e27f0219 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts @@ -10,13 +10,20 @@ import { transactionGroupsTransformer } from './transform'; describe('transactionGroupsTransformer', () => { it('should match snapshot', () => { - expect( - transactionGroupsTransformer({ - response: transactionGroupsResponse, - start: 100, - end: 2000, - }) - ).toMatchSnapshot(); + const { + bucketSize, + isAggregationAccurate, + items, + } = transactionGroupsTransformer({ + response: transactionGroupsResponse, + start: 100, + end: 2000, + bucketSize: 100, + }); + + expect(bucketSize).toBe(100); + expect(isAggregationAccurate).toBe(true); + expect(items).toMatchSnapshot(); }); it('should transform response correctly', () => { @@ -43,17 +50,59 @@ describe('transactionGroupsTransformer', () => { } as unknown) as ESResponse; expect( - transactionGroupsTransformer({ response, start: 100, end: 20000 }) - ).toEqual([ - { - averageResponseTime: 255966.30555555556, - impact: 0, - name: 'POST /api/orders', - p95: 320238.5, - sample: 'sample source', - transactionsPerMinute: 542.713567839196, + transactionGroupsTransformer({ + response, + start: 100, + end: 20000, + bucketSize: 100, + }) + ).toEqual({ + bucketSize: 100, + isAggregationAccurate: true, + items: [ + { + averageResponseTime: 255966.30555555556, + impact: 0, + name: 'POST /api/orders', + p95: 320238.5, + sample: 'sample source', + transactionsPerMinute: 542.713567839196, + }, + ], + }); + }); + + it('`isAggregationAccurate` should be false if number of bucket is higher than `bucketSize`', () => { + const bucket = { + key: { transaction: 'POST /api/orders' }, + doc_count: 180, + avg: { value: 255966.30555555556 }, + p95: { values: { '95.0': 320238.5 } }, + sum: { value: 3000000000 }, + sample: { + hits: { + total: 180, + hits: [{ _source: 'sample source' }], + }, }, - ]); + }; + + const response = ({ + aggregations: { + transaction_groups: { + buckets: [bucket, bucket, bucket, bucket], // four buckets returned + }, + }, + } as unknown) as ESResponse; + + const { isAggregationAccurate } = transactionGroupsTransformer({ + response, + start: 100, + end: 20000, + bucketSize: 3, // bucket size of three + }); + + expect(isAggregationAccurate).toEqual(false); }); it('should calculate impact from sum', () => { @@ -74,10 +123,13 @@ describe('transactionGroupsTransformer', () => { }, } as unknown) as ESResponse; - expect( - transactionGroupsTransformer({ response, start: 100, end: 20000 }).map( - (bucket) => bucket.impact - ) - ).toEqual([100, 25, 0]); + const { items } = transactionGroupsTransformer({ + response, + start: 100, + end: 20000, + bucketSize: 100, + }); + + expect(items.map((bucket) => bucket.impact)).toEqual([100, 25, 0]); }); }); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts index 2f34d365e5be9..81dba39e9d712 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts @@ -8,15 +8,15 @@ import moment from 'moment'; import { sortByOrder } from 'lodash'; import { ESResponse } from './fetcher'; -function calculateRelativeImpacts(transactionGroups: ITransactionGroup[]) { - const values = transactionGroups +function calculateRelativeImpacts(items: ITransactionGroup[]) { + const values = items .map(({ impact }) => impact) .filter((value) => value !== null) as number[]; const max = Math.max(...values); const min = Math.min(...values); - return transactionGroups.map((bucket) => ({ + return items.map((bucket) => ({ ...bucket, impact: bucket.impact !== null @@ -60,17 +60,30 @@ export function transactionGroupsTransformer({ response, start, end, + bucketSize, }: { response: ESResponse; start: number; end: number; -}): ITransactionGroup[] { + bucketSize: number; +}): { + items: ITransactionGroup[]; + isAggregationAccurate: boolean; + bucketSize: number; +} { const buckets = getBuckets(response); const duration = moment.duration(end - start); const minutes = duration.asMinutes(); - const transactionGroups = buckets.map((bucket) => - getTransactionGroup(bucket, minutes) - ); + const items = buckets.map((bucket) => getTransactionGroup(bucket, minutes)); - return calculateRelativeImpacts(transactionGroups); + const itemsWithRelativeImpact = calculateRelativeImpacts(items); + + return { + items: itemsWithRelativeImpact, + + // The aggregation is considered accurate if the configured bucket size is larger or equal to the number of buckets returned + // the actual number of buckets retrieved are `bucketsize + 1` to detect whether it's above the limit + isAggregationAccurate: bucketSize >= buckets.length, + bucketSize, + }; } diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index a34690aff43b4..02be2e7e4dcdf 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -76,6 +76,10 @@ import { rumPageViewsTrendRoute, rumPageLoadDistributionRoute, } from './rum_client'; +import { + observabilityDashboardHasDataRoute, + observabilityDashboardDataRoute, +} from './observability_dashboard'; const createApmApi = () => { const api = createApi() @@ -160,7 +164,11 @@ const createApmApi = () => { .add(rumOverviewLocalFiltersRoute) .add(rumPageViewsTrendRoute) .add(rumPageLoadDistributionRoute) - .add(rumClientMetricsRoute); + .add(rumClientMetricsRoute) + + // Observability dashboard + .add(observabilityDashboardHasDataRoute) + .add(observabilityDashboardDataRoute); return api; }; diff --git a/x-pack/plugins/apm/server/routes/observability_dashboard.ts b/x-pack/plugins/apm/server/routes/observability_dashboard.ts new file mode 100644 index 0000000000000..10c74295fe3e4 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/observability_dashboard.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { hasData } from '../lib/observability_dashboard/has_data'; +import { createRoute } from './create_route'; +import { rangeRt } from './default_api_types'; +import { getServiceCount } from '../lib/observability_dashboard/get_service_count'; +import { getTransactionCoordinates } from '../lib/observability_dashboard/get_transaction_coordinates'; + +export const observabilityDashboardHasDataRoute = createRoute(() => ({ + path: '/api/apm/observability_dashboard/has_data', + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + return await hasData({ setup }); + }, +})); + +export const observabilityDashboardDataRoute = createRoute(() => ({ + path: '/api/apm/observability_dashboard', + params: { + query: t.intersection([rangeRt, t.type({ bucketSize: t.string })]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { bucketSize } = context.params.query; + const serviceCountPromise = getServiceCount({ setup }); + const transactionCoordinatesPromise = getTransactionCoordinates({ + setup, + bucketSize, + }); + const [serviceCount, transactionCoordinates] = await Promise.all([ + serviceCountPromise, + transactionCoordinatesPromise, + ]); + return { serviceCount, transactionCoordinates }; + }, +})); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts new file mode 100644 index 0000000000000..620cabe652778 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; +import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; +import { CoreStart } from '../../../../../../src/core/public'; +import { KibanaURL } from './kibana_url'; +import * as shared from './shared'; + +export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; + +export interface PluginDeps { + discover: Pick; + embeddable: Pick; +} + +export interface CoreDeps { + application: Pick; +} + +export interface Params { + start: StartServicesGetter; +} + +export abstract class AbstractExploreDataAction { + public readonly getIconType = (context: Context): string => 'discoverApp'; + + public readonly getDisplayName = (context: Context): string => + i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', { + defaultMessage: 'Explore underlying data', + }); + + constructor(protected readonly params: Params) {} + + protected abstract async getUrl(context: Context): Promise; + + public async isCompatible({ embeddable }: Context): Promise { + if (!embeddable) return false; + if (!this.params.start().plugins.discover.urlGenerator) return false; + if (!shared.isVisualizeEmbeddable(embeddable)) return false; + if (!shared.getIndexPattern(embeddable)) return false; + if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; + return true; + } + + public async execute(context: Context): Promise { + if (!shared.isVisualizeEmbeddable(context.embeddable)) return; + + const { core } = this.params.start(); + const { appName, appPath } = await this.getUrl(context); + + await core.application.navigateToApp(appName, { + path: appPath, + }); + } + + public async getHref(context: Context): Promise { + const { embeddable } = context; + + if (!shared.isVisualizeEmbeddable(embeddable)) { + throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`); + } + + const { path } = await this.getUrl(context); + + return path; + } +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts new file mode 100644 index 0000000000000..a273f0d50e45e --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExploreDataChartAction } from './explore_data_chart_action'; +import { Params, PluginDeps } from './abstract_explore_data_action'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; +import { + EmbeddableStart, + RangeSelectTriggerContext, + ValueClickTriggerContext, + ChartActionContext, +} from '../../../../../../src/plugins/embeddable/public'; +import { i18n } from '@kbn/i18n'; +import { + VisualizeEmbeddableContract, + VISUALIZE_EMBEDDABLE_TYPE, +} from '../../../../../../src/plugins/visualizations/public'; +import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { Filter, TimeRange } from '../../../../../../src/plugins/data/public'; + +const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; + +jest.mock('@kbn/i18n', () => ({ + i18n: { + translate: jest.fn((key, options) => options.defaultMessage), + }, +})); + +afterEach(() => { + i18nTranslateSpy.mockClear(); +}); + +const setup = ({ useRangeEvent = false }: { useRangeEvent?: boolean } = {}) => { + type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + + const core = coreMock.createStart(); + + const urlGenerator: UrlGenerator = ({ + createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), + } as unknown) as UrlGenerator; + + const filtersAndTimeRangeFromContext = jest.fn((async () => ({ + filters: [], + })) as EmbeddableStart['filtersAndTimeRangeFromContext']); + + const plugins: PluginDeps = { + discover: { + urlGenerator, + }, + embeddable: { + filtersAndTimeRangeFromContext, + }, + }; + + const params: Params = { + start: () => ({ + plugins, + self: {}, + core, + }), + }; + const action = new ExploreDataChartAction(params); + + const input = { + viewMode: ViewMode.VIEW, + }; + + const output = { + indexPatterns: [ + { + id: 'index-ptr-foo', + }, + ], + }; + + const embeddable: VisualizeEmbeddableContract = ({ + type: VISUALIZE_EMBEDDABLE_TYPE, + getInput: () => input, + getOutput: () => output, + } as unknown) as VisualizeEmbeddableContract; + + const data: ChartActionContext['data'] = { + ...(useRangeEvent + ? ({ range: {} } as RangeSelectTriggerContext['data']) + : ({ data: [] } as ValueClickTriggerContext['data'])), + timeFieldName: 'order_date', + }; + + const context = { + embeddable, + data, + } as ChartActionContext; + + return { core, plugins, urlGenerator, params, action, input, output, embeddable, data, context }; +}; + +describe('"Explore underlying data" panel action', () => { + test('action has Discover icon', () => { + const { action, context } = setup(); + expect(action.getIconType(context)).toBe('discoverApp'); + }); + + test('title is "Explore underlying data"', () => { + const { action, context } = setup(); + expect(action.getDisplayName(context)).toBe('Explore underlying data'); + }); + + test('translates title', () => { + expect(i18nTranslateSpy).toHaveBeenCalledTimes(0); + + const { action, context } = setup(); + action.getDisplayName(context); + + expect(i18nTranslateSpy).toHaveBeenCalledTimes(1); + expect(i18nTranslateSpy.mock.calls[0][0]).toBe( + 'xpack.discover.FlyoutCreateDrilldownAction.displayName' + ); + }); + + describe('isCompatible()', () => { + test('returns true when all conditions are met', async () => { + const { action, context } = setup(); + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(true); + }); + + test('returns false when URL generator is not present', async () => { + const { action, plugins, context } = setup(); + (plugins.discover as any).urlGenerator = undefined; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable is not Visualize embeddable', async () => { + const { action, embeddable, context } = setup(); + (embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE'; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable does not have index patterns', async () => { + const { action, output, context } = setup(); + delete output.indexPatterns; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable index patterns are empty', async () => { + const { action, output, context } = setup(); + output.indexPatterns = []; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if dashboard is in edit mode', async () => { + const { action, input, context } = setup(); + input.viewMode = ViewMode.EDIT; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + }); + + describe('getHref()', () => { + test('returns URL path generated by URL generator', async () => { + const { action, context } = setup(); + + const href = await action.getHref(context); + + expect(href).toBe('/xyz/app/discover/foo#bar'); + }); + + test('calls URL generator with right arguments', async () => { + const { action, urlGenerator, context } = setup(); + + expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + + await action.getHref(context); + + expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); + expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + filters: [], + indexPatternId: 'index-ptr-foo', + timeRange: undefined, + }); + }); + + test('applies chart event filters', async () => { + const { action, context, urlGenerator, plugins } = setup(); + + ((plugins.embeddable + .filtersAndTimeRangeFromContext as unknown) as jest.SpyInstance).mockImplementation(() => { + const filters: Filter[] = [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + ]; + const timeRange: TimeRange = { + from: 'from', + to: 'to', + }; + return { filters, timeRange }; + }); + + expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(0); + + await action.getHref(context); + + expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(1); + expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledWith(context); + expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + filters: [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + ], + indexPatternId: 'index-ptr-foo', + timeRange: { + from: 'from', + to: 'to', + }, + }); + }); + }); + + describe('execute()', () => { + test('calls platform SPA navigation method', async () => { + const { action, context, core } = setup(); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(0); + + await action.execute(context); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(1); + }); + + test('calls platform SPA navigation method with right arguments', async () => { + const { action, context, core } = setup(); + + await action.execute(context); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(1); + expect(core.application.navigateToApp.mock.calls[0]).toEqual([ + 'discover', + { + path: '/foo#bar', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts new file mode 100644 index 0000000000000..359f14959c6a6 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { + ValueClickTriggerContext, + RangeSelectTriggerContext, +} from '../../../../../../src/plugins/embeddable/public'; +import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; +import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { KibanaURL } from './kibana_url'; +import * as shared from './shared'; +import { AbstractExploreDataAction } from './abstract_explore_data_action'; + +export type ExploreDataChartActionContext = ValueClickTriggerContext | RangeSelectTriggerContext; + +export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART'; + +/** + * This is "Explore underlying data" action which appears in popup context + * menu when user clicks a value in visualization or brushes a time range. + */ +export class ExploreDataChartAction extends AbstractExploreDataAction + implements Action { + public readonly id = ACTION_EXPLORE_DATA_CHART; + + public readonly type = ACTION_EXPLORE_DATA_CHART; + + public readonly order = 200; + + protected readonly getUrl = async ( + context: ExploreDataChartActionContext + ): Promise => { + const { plugins } = this.params.start(); + const { urlGenerator } = plugins.discover; + + if (!urlGenerator) { + throw new Error('Discover URL generator not available.'); + } + + const { embeddable } = context; + const { filters, timeRange } = await plugins.embeddable.filtersAndTimeRangeFromContext(context); + const state: DiscoverUrlGeneratorState = { + filters, + timeRange, + }; + + if (embeddable) { + state.indexPatternId = shared.getIndexPattern(embeddable) || undefined; + + const input = embeddable.getInput(); + + if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange; + if (isQuery(input.query)) state.query = input.query; + if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])]; + } + + const path = await urlGenerator.createUrl(state); + + return new KibanaURL(path); + }; +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts similarity index 88% rename from x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts rename to x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts index a7167d2e2e691..e742b69380973 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ExploreDataContextMenuAction, - ACTION_EXPLORE_DATA, - Params, - PluginDeps, -} from './explore_data_context_menu_action'; +import { ExploreDataContextMenuAction } from './explore_data_context_menu_action'; +import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; +import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, @@ -37,14 +34,20 @@ const setup = () => { const core = coreMock.createStart(); const urlGenerator: UrlGenerator = ({ - id: ACTION_EXPLORE_DATA, createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; + const filtersAndTimeRangeFromContext = jest.fn((async () => ({ + filters: [], + })) as EmbeddableStart['filtersAndTimeRangeFromContext']); + const plugins: PluginDeps = { discover: { urlGenerator, }, + embeddable: { + filtersAndTimeRangeFromContext, + }, }; const params: Params = { @@ -83,19 +86,20 @@ const setup = () => { describe('"Explore underlying data" panel action', () => { test('action has Discover icon', () => { - const { action } = setup(); - expect(action.getIconType()).toBe('discoverApp'); + const { action, context } = setup(); + expect(action.getIconType(context)).toBe('discoverApp'); }); test('title is "Explore underlying data"', () => { - const { action } = setup(); - expect(action.getDisplayName()).toBe('Explore underlying data'); + const { action, context } = setup(); + expect(action.getDisplayName(context)).toBe('Explore underlying data'); }); test('translates title', () => { expect(i18nTranslateSpy).toHaveBeenCalledTimes(0); - setup().action.getDisplayName(); + const { action, context } = setup(); + action.getDisplayName(context); expect(i18nTranslateSpy).toHaveBeenCalledTimes(1); expect(i18nTranslateSpy.mock.calls[0][0]).toBe( diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts new file mode 100644 index 0000000000000..6691089f875d8 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { EmbeddableContext } from '../../../../../../src/plugins/embeddable/public'; +import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; +import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { KibanaURL } from './kibana_url'; +import * as shared from './shared'; +import { AbstractExploreDataAction } from './abstract_explore_data_action'; + +export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; + +/** + * This is "Explore underlying data" action which appears in the context + * menu of a dashboard panel. + */ +export class ExploreDataContextMenuAction extends AbstractExploreDataAction + implements Action { + public readonly id = ACTION_EXPLORE_DATA; + + public readonly type = ACTION_EXPLORE_DATA; + + public readonly order = 200; + + protected readonly getUrl = async (context: EmbeddableContext): Promise => { + const { plugins } = this.params.start(); + const { urlGenerator } = plugins.discover; + + if (!urlGenerator) { + throw new Error('Discover URL generator not available.'); + } + + const { embeddable } = context; + const state: DiscoverUrlGeneratorState = {}; + + if (embeddable) { + state.indexPatternId = shared.getIndexPattern(embeddable) || undefined; + + const input = embeddable.getInput(); + + if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange; + if (isQuery(input.query)) state.query = input.query; + if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])]; + } + + const path = await urlGenerator.createUrl(state); + + return new KibanaURL(path); + }; +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts similarity index 86% rename from x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts rename to x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts index 8788621365385..e6d7d4b59149e 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts @@ -5,3 +5,4 @@ */ export * from './explore_data_context_menu_action'; +export * from './explore_data_chart_action'; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts new file mode 100644 index 0000000000000..3c25fc2b3c3d1 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// TODO: Replace this logic with KibanaURL once it is available. +// https://github.com/elastic/kibana/issues/64497 +export class KibanaURL { + public readonly path: string; + public readonly appName: string; + public readonly appPath: string; + + constructor(path: string) { + const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/); + + if (!match) { + throw new Error('Unexpected Discover URL path.'); + } + + const [, appName, appPath] = match; + + if (!appName || !appPath) { + throw new Error('Could not parse Discover URL path.'); + } + + this.path = path; + this.appName = appName; + this.appPath = appPath; + } +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts new file mode 100644 index 0000000000000..fa2168df944b0 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { + VISUALIZE_EMBEDDABLE_TYPE, + VisualizeEmbeddableContract, +} from '../../../../../../src/plugins/visualizations/public'; + +export const isOutputWithIndexPatterns = ( + output: unknown +): output is { indexPatterns: Array<{ id: string }> } => { + if (!output || typeof output !== 'object') return false; + return Array.isArray((output as any).indexPatterns); +}; + +export const isVisualizeEmbeddable = ( + embeddable?: IEmbeddable +): embeddable is VisualizeEmbeddableContract => + embeddable && embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE ? true : false; + +/** + * @returns Returns empty string if no index pattern ID found. + */ +export const getIndexPattern = (embeddable?: IEmbeddable): string => { + if (!embeddable) return ''; + const output = embeddable.getOutput(); + + if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { + return output.indexPatterns[0].id; + } + + return ''; +}; diff --git a/x-pack/plugins/discover_enhanced/public/actions/index.ts b/x-pack/plugins/discover_enhanced/public/actions/index.ts index cbb955fa46340..209ae6bee09b5 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/index.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './view_in_discover'; +export * from './explore_data'; diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts deleted file mode 100644 index d66ca129934a8..0000000000000 --- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable max-classes-per-file */ - -import { i18n } from '@kbn/i18n'; -import { Action } from '../../../../../../src/plugins/ui_actions/public'; -import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; -import { - EmbeddableContext, - IEmbeddable, - ViewMode, -} from '../../../../../../src/plugins/embeddable/public'; -import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; -import { CoreStart } from '../../../../../../src/core/public'; -import { - VisualizeEmbeddableContract, - VISUALIZE_EMBEDDABLE_TYPE, -} from '../../../../../../src/plugins/visualizations/public'; - -// TODO: Replace this logic with KibanaURL once it is available. -// https://github.com/elastic/kibana/issues/64497 -class KibanaURL { - public readonly path: string; - public readonly appName: string; - public readonly appPath: string; - - constructor(path: string) { - const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/); - - if (!match) { - throw new Error('Unexpected Discover URL path.'); - } - - const [, appName, appPath] = match; - - if (!appName || !appPath) { - throw new Error('Could not parse Discover URL path.'); - } - - this.path = path; - this.appName = appName; - this.appPath = appPath; - } -} - -export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; - -const isOutputWithIndexPatterns = ( - output: unknown -): output is { indexPatterns: Array<{ id: string }> } => { - if (!output || typeof output !== 'object') return false; - return Array.isArray((output as any).indexPatterns); -}; - -const isVisualizeEmbeddable = ( - embeddable: IEmbeddable -): embeddable is VisualizeEmbeddableContract => embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE; - -export interface PluginDeps { - discover: Pick; -} - -export interface CoreDeps { - application: Pick; -} - -export interface Params { - start: StartServicesGetter; -} - -export class ExploreDataContextMenuAction implements Action { - public readonly id = ACTION_EXPLORE_DATA; - - public readonly type = ACTION_EXPLORE_DATA; - - public readonly order = 200; - - constructor(private readonly params: Params) {} - - public getDisplayName() { - return i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', { - defaultMessage: 'Explore underlying data', - }); - } - - public getIconType() { - return 'discoverApp'; - } - - public async isCompatible({ embeddable }: EmbeddableContext) { - if (!this.params.start().plugins.discover.urlGenerator) return false; - if (!isVisualizeEmbeddable(embeddable)) return false; - if (!this.getIndexPattern(embeddable)) return false; - if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; - return true; - } - - public async execute({ embeddable }: EmbeddableContext) { - if (!isVisualizeEmbeddable(embeddable)) return; - - const { core } = this.params.start(); - const { appName, appPath } = await this.getUrl(embeddable); - - await core.application.navigateToApp(appName, { - path: appPath, - }); - } - - public async getHref({ embeddable }: EmbeddableContext): Promise { - if (!isVisualizeEmbeddable(embeddable)) { - throw new Error(`Embeddable not supported for "${this.getDisplayName()}" action.`); - } - - const { path } = await this.getUrl(embeddable); - - return path; - } - - private async getUrl(embeddable: VisualizeEmbeddableContract): Promise { - const { plugins } = this.params.start(); - const { urlGenerator } = plugins.discover; - - if (!urlGenerator) { - throw new Error('Discover URL generator not available.'); - } - - const { timeRange, query, filters } = embeddable.getInput(); - const indexPatternId = this.getIndexPattern(embeddable); - - const path = await urlGenerator.createUrl({ - indexPatternId, - filters, - query, - timeRange, - }); - - return new KibanaURL(path); - } - - /** - * @returns Returns empty string if no index pattern ID found. - */ - private getIndexPattern(embeddable: VisualizeEmbeddableContract): string { - const output = embeddable!.getOutput(); - - if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { - return output.indexPatterns[0].id; - } - - return ''; - } -} diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts index f55c5dab3449b..ea3c1222eb369 100644 --- a/x-pack/plugins/discover_enhanced/public/plugin.ts +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -6,7 +6,12 @@ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { PluginInitializerContext } from 'kibana/public'; -import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { + UiActionsSetup, + UiActionsStart, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../src/plugins/ui_actions/public'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; @@ -16,11 +21,18 @@ import { EmbeddableContext, CONTEXT_MENU_TRIGGER, } from '../../../../src/plugins/embeddable/public'; -import { ExploreDataContextMenuAction, ACTION_EXPLORE_DATA } from './actions'; +import { + ExploreDataContextMenuAction, + ExploreDataChartAction, + ACTION_EXPLORE_DATA, + ACTION_EXPLORE_DATA_CHART, + ExploreDataChartActionContext, +} from './actions'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { [ACTION_EXPLORE_DATA]: EmbeddableContext; + [ACTION_EXPLORE_DATA_CHART]: ExploreDataChartActionContext; } } @@ -48,10 +60,17 @@ export class DiscoverEnhancedPlugin { uiActions, share }: DiscoverEnhancedSetupDependencies ) { const start = createStartServicesGetter(core.getStartServices); + const isSharePluginInstalled = !!share; - if (!!share) { - const exploreDataAction = new ExploreDataContextMenuAction({ start }); + if (isSharePluginInstalled) { + const params = { start }; + + const exploreDataAction = new ExploreDataContextMenuAction(params); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, exploreDataAction); + + const exploreDataChartAction = new ExploreDataChartAction(params); + uiActions.addTriggerAction(SELECT_RANGE_TRIGGER, exploreDataChartAction); + uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, exploreDataChartAction); } } diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts index c2e8cd6129d80..3d1efc8a479b2 100644 --- a/x-pack/plugins/features/server/routes/index.test.ts +++ b/x-pack/plugins/features/server/routes/index.test.ts @@ -16,9 +16,7 @@ import { FeatureConfig } from '../../common'; function createContextMock(licenseType: LicenseType = 'gold') { return { core: coreMock.createRequestHandlerContext(), - licensing: { - license: licensingMock.createLicense({ license: { type: licenseType } }), - }, + licensing: licensingMock.createRequestHandlerContext({ license: { type: licenseType } }), }; } diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx index 47a0f037816bc..04642a01c15b4 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx @@ -7,6 +7,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; @@ -15,6 +16,9 @@ export const InventoryAlertDropdown = () => { const [flyoutVisible, setFlyoutVisible] = useState(false); const kibana = useKibana(); + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric, filterQuery } = inventoryPrefill; + const closePopover = useCallback(() => { setPopoverOpen(false); }, [setPopoverOpen]); @@ -57,7 +61,13 @@ export const InventoryAlertDropdown = () => { > - + ); }; diff --git a/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts new file mode 100644 index 0000000000000..d659057b95ed9 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; +import { SnapshotMetricInput } from '../../../../common/http_api/snapshot_api'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; + +export const useInventoryAlertPrefill = () => { + const [nodeType, setNodeType] = useState('host'); + const [filterQuery, setFilterQuery] = useState(); + const [metric, setMetric] = useState({ type: 'cpu' }); + + return { + nodeType, + filterQuery, + metric, + setNodeType, + setFilterQuery, + setMetric, + }; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx index d26575f65dfec..384a93e796dbe 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -7,14 +7,18 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { AlertFlyout } from './alert_flyout'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useAlertPrefillContext } from '../../use_alert_prefill'; +import { AlertFlyout } from './alert_flyout'; export const MetricsAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); const kibana = useKibana(); + const { metricThresholdPrefill } = useAlertPrefillContext(); + const { groupBy, filterQuery, metrics } = metricThresholdPrefill; + const closePopover = useCallback(() => { setPopoverOpen(false); }, [setPopoverOpen]); @@ -57,7 +61,11 @@ export const MetricsAlertDropdown = () => { > - + ); }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx new file mode 100644 index 0000000000000..f6119107ac133 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; +import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { AlertContextMeta } from '../types'; +import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; +import React from 'react'; +import { Expressions } from './expression'; +import { act } from 'react-dom/test-utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; + +jest.mock('../../../containers/source/use_source_via_http', () => ({ + useSourceViaHttp: () => ({ + source: { id: 'default' }, + createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), + }), +})); + +describe('Expression', () => { + async function setup(currentOptions: { + metrics?: MetricsExplorerMetric[]; + filterQuery?: string; + groupBy?: string; + }) { + const alertParams = { + criteria: [], + groupBy: undefined, + filterQueryText: '', + }; + + const mocks = coreMock.createSetup(); + const startMocks = coreMock.createStart(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + const context: AlertsContextValue = { + http: mocks.http, + toastNotifications: mocks.notifications.toasts, + actionTypeRegistry: actionTypeRegistryMock.create() as any, + alertTypeRegistry: alertTypeRegistryMock.create() as any, + docLinks: startMocks.docLinks, + capabilities: { + ...capabilities, + actions: { + delete: true, + save: true, + show: true, + }, + }, + metadata: { + currentOptions, + }, + }; + + const wrapper = mountWithIntl( + Reflect.set(alertParams, key, value)} + setAlertProperty={() => {}} + /> + ); + + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await update(); + + return { wrapper, update, alertParams }; + } + + it('should prefill the alert using the context metadata', async () => { + const currentOptions = { + groupBy: 'host.hostname', + filterQuery: 'foo', + metrics: [ + { aggregation: 'avg', field: 'system.load.1' }, + { aggregation: 'cardinality', field: 'system.cpu.user.pct' }, + ] as MetricsExplorerMetric[], + }; + const { alertParams } = await setup(currentOptions); + expect(alertParams.groupBy).toBe('host.hostname'); + expect(alertParams.filterQueryText).toBe('foo'); + expect(alertParams.criteria).toEqual([ + { + metric: 'system.load.1', + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', + aggType: 'avg', + }, + { + metric: 'system.cpu.user.pct', + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', + aggType: 'cardinality', + }, + ]); + }); +}); 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 3c3351f4ddd76..f45474f284484 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { debounce, pick } from 'lodash'; +import { debounce, pick, omit } from 'lodash'; import { Unit } from '@elastic/datemath'; import * as rt from 'io-ts'; import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react'; @@ -52,7 +52,7 @@ import { useSourceViaHttp } from '../../../containers/source/use_source_via_http import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; import { ExpressionRow } from './expression_row'; -import { AlertContextMeta, TimeUnit, MetricExpression } from '../types'; +import { AlertContextMeta, TimeUnit, MetricExpression, AlertParams } from '../types'; import { ExpressionChart } from './expression_chart'; import { validateMetricThreshold } from './validation'; @@ -60,14 +60,7 @@ const FILTER_TYPING_DEBOUNCE_MS = 500; interface Props { errors: IErrorObject[]; - alertParams: { - criteria: MetricExpression[]; - groupBy?: string; - filterQuery?: string; - sourceId?: string; - filterQueryText?: string; - alertOnNoData?: boolean; - }; + alertParams: AlertParams; alertsContext: AlertsContextValue; alertInterval: string; setAlertParams(key: string, value: any): void; @@ -81,6 +74,7 @@ const defaultExpression = { timeSize: 1, timeUnit: 'm', } as MetricExpression; +export { defaultExpression }; export const Expressions: React.FC = (props) => { const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props; @@ -247,6 +241,13 @@ export const Expressions: React.FC = (props) => { } }, [alertsContext.metadata, derivedIndexPattern, setAlertParams]); + const preFillAlertGroupBy = useCallback(() => { + const md = alertsContext.metadata; + if (md && md.currentOptions?.groupBy && !md.series) { + setAlertParams('groupBy', md.currentOptions.groupBy); + } + }, [alertsContext.metadata, setAlertParams]); + const onSelectPreviewLookbackInterval = useCallback((e) => { setPreviewLookbackInterval(e.target.value); setPreviewResult(null); @@ -286,6 +287,10 @@ export const Expressions: React.FC = (props) => { preFillAlertFilter(); } + if (!alertParams.groupBy) { + preFillAlertGroupBy(); + } + if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } @@ -465,7 +470,7 @@ export const Expressions: React.FC = (props) => { id="selectPreviewLookbackInterval" value={previewLookbackInterval} onChange={onSelectPreviewLookbackInterval} - options={previewOptions} + options={previewDOMOptions} /> @@ -588,6 +593,10 @@ export const Expressions: React.FC = (props) => { ); }; +const previewDOMOptions: Array<{ text: string; value: string }> = previewOptions.map((o) => + omit(o, 'shortText') +); + // required for dynamic import // eslint-disable-next-line import/no-default-export export default Expressions; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx index da342f0a45420..2221d3cd4fe12 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx @@ -50,7 +50,7 @@ export function validateMetricThreshold({ if (!c.aggType) { errors[id].aggField.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', { - defaultMessage: 'Aggreation is required.', + defaultMessage: 'Aggregation is required.', }) ); } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts new file mode 100644 index 0000000000000..366d6aa7003e6 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +import { useState } from 'react'; +import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; + +interface MetricThresholdPrefillOptions { + groupBy: string | string[] | undefined; + filterQuery: string | undefined; + metrics: MetricsExplorerMetric[]; +} + +export const useMetricThresholdAlertPrefill = () => { + const [prefillOptionsState, setPrefillOptionsState] = useState({ + groupBy: undefined, + filterQuery: undefined, + metrics: [], + }); + + const { groupBy, filterQuery, metrics } = prefillOptionsState; + + return { + groupBy, + filterQuery, + metrics, + setPrefillOptions(newState: MetricThresholdPrefillOptions) { + if (!isEqual(newState, prefillOptionsState)) setPrefillOptionsState(newState); + }, + }; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index feeec4b0ce8bf..2f8d7ec0ba6f4 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -51,3 +51,12 @@ export interface ExpressionChartData { id: string; series: ExpressionChartSeries; } + +export interface AlertParams { + criteria: MetricExpression[]; + groupBy?: string; + filterQuery?: string; + sourceId?: string; + filterQueryText?: string; + alertOnNoData?: boolean; +} diff --git a/x-pack/plugins/infra/public/alerting/use_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/use_alert_prefill.ts new file mode 100644 index 0000000000000..eff2fe462509f --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/use_alert_prefill.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import createContainer from 'constate'; +import { useMetricThresholdAlertPrefill } from './metric_threshold/hooks/use_metric_threshold_alert_prefill'; +import { useInventoryAlertPrefill } from './inventory/hooks/use_inventory_alert_prefill'; + +const useAlertPrefill = () => { + const metricThresholdPrefill = useMetricThresholdAlertPrefill(); + const inventoryPrefill = useInventoryAlertPrefill(); + + return { metricThresholdPrefill, inventoryPrefill }; +}; + +export const [AlertPrefillProvider, useAlertPrefillContext] = createContainer(useAlertPrefill); diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index a04897d9c738d..2c76b3bb925ee 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -59,7 +59,7 @@ class WithKueryAutocompletionComponent extends React.Component< ) => { const { indexPattern } = this.props; const language = 'kuery'; - const hasQuerySuggestions = this.props.kibana.services.data.autocomplete.hasQuerySuggestions( + const hasQuerySuggestions = this.props.kibana.services.data?.autocomplete.hasQuerySuggestions( language ); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index ab7f41e3066b8..121748f8e5220 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -31,6 +31,7 @@ import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown'; import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; +import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', { defaultMessage: 'Add data', @@ -44,114 +45,119 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { return ( - - - - - - - + + + + + + -
- - - - - - - - - - - - {ADD_DATA_LABEL} - - - - + - - - ( - - {({ configuration, createDerivedIndexPattern }) => ( - - - {configuration ? ( - - ) : ( - - )} - - )} - - )} +
- - - - - - + + + + + + + + + + + + {ADD_DATA_LABEL} + + + + + + + + ( + + {({ configuration, createDerivedIndexPattern }) => ( + + + {configuration ? ( + + ) : ( + + )} + + )} + + )} + /> + + + + + + + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts new file mode 100644 index 0000000000000..93b6b635183dd --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useWaffleFilters, WaffleFiltersState } from './use_waffle_filters'; + +// Mock useUrlState hook +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + location: '', + replace: () => {}, + }), +})); + +jest.mock('../../../../containers/source', () => ({ + useSourceContext: () => ({ + createDerivedIndexPattern: () => 'jestbeat-*', + }), +})); + +let PREFILL: Record = {}; +jest.mock('../../../../alerting/use_alert_prefill', () => ({ + useAlertPrefillContext: () => ({ + inventoryPrefill: { + setFilterQuery(filterQuery: string) { + PREFILL = { filterQuery }; + }, + }, + }), +})); + +const renderUseWaffleFiltersHook = () => renderHook(() => useWaffleFilters()); + +describe('useWaffleFilters', () => { + beforeEach(() => { + PREFILL = {}; + }); + + it('should sync the options to the inventory alert preview context', () => { + const { result, rerender } = renderUseWaffleFiltersHook(); + + const newQuery = { + expression: 'foo', + kind: 'kuery', + } as WaffleFiltersState; + act(() => { + result.current.applyFilterQuery(newQuery); + }); + rerender(); + expect(PREFILL.filterQuery).toEqual(newQuery.expression); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts index 63d9d08796f05..d4fb1356be77e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts @@ -10,6 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainter from 'constate'; +import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { useUrlState } from '../../../../utils/use_url_state'; import { useSourceContext } from '../../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery'; @@ -68,6 +69,10 @@ export const useWaffleFilters = () => { filterQueryDraft, ]); + const { inventoryPrefill } = useAlertPrefillContext(); + const prefillContext = useMemo(() => inventoryPrefill, [inventoryPrefill]); // For Jest compatibility + useEffect(() => prefillContext.setFilterQuery(state.expression), [prefillContext, state]); + return { filterQuery: urlState, filterQueryDraft, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts new file mode 100644 index 0000000000000..579073e9500d0 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useWaffleOptions, WaffleOptionsState } from './use_waffle_options'; + +// Mock useUrlState hook +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + location: '', + replace: () => {}, + }), +})); + +// Jest can't access variables outside the scope of the mock factory function except to +// reassign them, so we can't make these both part of the same object +let PREFILL_NODETYPE: WaffleOptionsState['nodeType'] | undefined; +let PREFILL_METRIC: WaffleOptionsState['metric'] | undefined; +jest.mock('../../../../alerting/use_alert_prefill', () => ({ + useAlertPrefillContext: () => ({ + inventoryPrefill: { + setNodeType(nodeType: WaffleOptionsState['nodeType']) { + PREFILL_NODETYPE = nodeType; + }, + setMetric(metric: WaffleOptionsState['metric']) { + PREFILL_METRIC = metric; + }, + }, + }), +})); + +const renderUseWaffleOptionsHook = () => renderHook(() => useWaffleOptions()); + +describe('useWaffleOptions', () => { + beforeEach(() => { + PREFILL_NODETYPE = undefined; + PREFILL_METRIC = undefined; + }); + + it('should sync the options to the inventory alert preview context', () => { + const { result, rerender } = renderUseWaffleOptionsHook(); + + const newOptions = { + nodeType: 'pod', + metric: { type: 'memory' }, + } as WaffleOptionsState; + act(() => { + result.current.changeNodeType(newOptions.nodeType); + }); + rerender(); + expect(PREFILL_NODETYPE).toEqual(newOptions.nodeType); + act(() => { + result.current.changeMetric(newOptions.metric); + }); + rerender(); + expect(PREFILL_METRIC).toEqual(newOptions.metric); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts index 975e33cf2415f..a3132c8384979 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts @@ -10,6 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainer from 'constate'; +import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { InventoryColorPaletteRT } from '../../../../lib/lib'; import { SnapshotMetricInput, @@ -121,6 +122,13 @@ export const useWaffleOptions = () => { [setState] ); + const { inventoryPrefill } = useAlertPrefillContext(); + useEffect(() => { + const { setNodeType, setMetric } = inventoryPrefill; + setNodeType(state.nodeType); + setMetric(state.metric); + }, [state, inventoryPrefill]); + return { ...DEFAULT_WAFFLE_OPTIONS_STATE, ...state, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx index 1381ed9da656a..c35e9f17bdcc3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx @@ -4,26 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useMetricsExplorerOptions, - MetricsExplorerOptionsContainer, MetricsExplorerOptions, MetricsExplorerTimeOptions, DEFAULT_OPTIONS, DEFAULT_TIMERANGE, } from './use_metrics_explorer_options'; -const renderUseMetricsExplorerOptionsHook = () => - renderHook(() => useMetricsExplorerOptions(), { - initialProps: {}, - wrapper: ({ children }) => ( - - {children} - - ), - }); +let PREFILL: Record = {}; +jest.mock('../../../../alerting/use_alert_prefill', () => ({ + useAlertPrefillContext: () => ({ + metricThresholdPrefill: { + setPrefillOptions(opts: Record) { + PREFILL = opts; + }, + }, + }), +})); + +const renderUseMetricsExplorerOptionsHook = () => renderHook(() => useMetricsExplorerOptions()); interface LocalStore { [key: string]: string; @@ -52,6 +53,7 @@ describe('useMetricExplorerOptions', () => { beforeEach(() => { delete STORE.MetricsExplorerOptions; delete STORE.MetricsExplorerTimeRange; + PREFILL = {}; }); it('should just work', () => { @@ -100,4 +102,22 @@ describe('useMetricExplorerOptions', () => { const { result } = renderUseMetricsExplorerOptionsHook(); expect(result.current.options).toEqual(newOptions); }); + + it('should sync the options to the threshold alert preview context', () => { + const { result, rerender } = renderUseMetricsExplorerOptionsHook(); + + const newOptions: MetricsExplorerOptions = { + ...DEFAULT_OPTIONS, + metrics: [{ aggregation: 'count' }], + filterQuery: 'foo', + groupBy: 'host.hostname', + }; + act(() => { + result.current.setOptions(newOptions); + }); + rerender(); + expect(PREFILL.metrics).toEqual(newOptions.metrics); + expect(PREFILL.groupBy).toEqual(newOptions.groupBy); + expect(PREFILL.filterQuery).toEqual(newOptions.filterQuery); + }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 56595c09aadde..8abdffd39ed3a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -5,7 +5,8 @@ */ import createContainer from 'constate'; -import { useState, useEffect, Dispatch, SetStateAction } from 'react'; +import { useState, useEffect, useMemo, Dispatch, SetStateAction } from 'react'; +import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { MetricsExplorerColor } from '../../../../../common/color_palette'; import { MetricsExplorerAggregation, @@ -122,6 +123,21 @@ export const useMetricsExplorerOptions = () => { DEFAULT_CHART_OPTIONS ); const [isAutoReloading, setAutoReloading] = useState(false); + + const { metricThresholdPrefill } = useAlertPrefillContext(); + // For Jest compatibility; including metricThresholdPrefill as a dep in useEffect causes an + // infinite loop in test environment + const prefillContext = useMemo(() => metricThresholdPrefill, [metricThresholdPrefill]); + + useEffect(() => { + if (prefillContext) { + const { setPrefillOptions } = prefillContext; + const { metrics, groupBy, filterQuery } = options; + + setPrefillOptions({ metrics, groupBy, filterQuery }); + } + }, [options, prefillContext]); + return { defaultViewState: { options: DEFAULT_OPTIONS, diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index 4d60b9031414e..ebcce6320ec4b 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -36,7 +36,7 @@ export { PACKAGES_SAVED_OBJECT_TYPE, INDEX_PATTERN_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - GLOBAL_SETTINGS_SAVED_OBJECT_TYPE as GLOBAL_SETTINGS_SAVED_OBJET_TYPE, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, // Defaults DEFAULT_AGENT_CONFIG, DEFAULT_OUTPUT, diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 703ddb521c831..1199c9d198e3a 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -15,7 +15,7 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - GLOBAL_SETTINGS_SAVED_OBJET_TYPE, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, } from '../constants'; import { migrateDatasourcesToV790 } from './migrations/datasources_v790'; import { migrateAgentConfigToV790 } from './migrations/agent_config_v790'; @@ -26,8 +26,8 @@ import { migrateAgentConfigToV790 } from './migrations/agent_config_v790'; */ const savedObjectTypes: { [key: string]: SavedObjectsType } = { - [GLOBAL_SETTINGS_SAVED_OBJET_TYPE]: { - name: GLOBAL_SETTINGS_SAVED_OBJET_TYPE, + [GLOBAL_SETTINGS_SAVED_OBJECT_TYPE]: { + name: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, hidden: false, namespaceType: 'agnostic', management: { @@ -246,7 +246,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { internal: { type: 'boolean' }, removable: { type: 'boolean' }, es_index_patterns: { - dynamic: 'false', + enabled: false, type: 'object', }, installed: { diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 7a33241792a58..1ee618a31a698 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -38,7 +38,7 @@ export async function mountApp( const stateTransfer = embeddable?.getStateTransfer(params.history); const { originatingApp } = - stateTransfer?.getIncomingOriginatingApp({ keysToRemoveAfterFetch: ['originatingApp'] }) || {}; + stateTransfer?.getIncomingEditorState({ keysToRemoveAfterFetch: ['originatingApp'] }) || {}; const instance = await createEditorFrame(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss index 4ba19cb4ab05b..e663754707e05 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss @@ -10,11 +10,14 @@ .lnsWorkspacePanelWrapper__pageContentHeader { @include euiTitle('xs'); padding: $euiSizeM; - border-bottom: $euiBorderThin; // override EuiPage margin-bottom: 0 !important; // sass-lint:disable-line no-important } + .lnsWorkspacePanelWrapper__pageContentHeader--unsaved { + color: $euiTextSubduedColor; + } + .lnsWorkspacePanelWrapper__pageContentBody { @include euiScrollBar; flex-grow: 1; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss index 254807d06d386..691cda9ff0d79 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss @@ -1,9 +1,11 @@ .lnsDimensionPopover { line-height: 0; flex-grow: 1; + max-width: calc(100% - #{$euiSizeL}); } .lnsDimensionPopover__trigger { max-width: 100%; display: block; + word-break: break-word; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 07c76a81ed62d..af3d0ed068d2f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -23,7 +23,6 @@ import { WorkspacePanel } from './workspace_panel'; import { Document } from '../../persistence/saved_object_store'; import { RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; -import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; import { EditorFrameStartPlugins } from '../service'; @@ -275,21 +274,20 @@ export function EditorFrame(props: EditorFrameProps) { } workspacePanel={ allLoaded && ( - - - + ) } suggestionsPanel={ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx index e4d37772eac2e..670afe28293a4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx @@ -37,6 +37,7 @@ import { trackUiEvent } from '../../lens_ui_telemetry'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; +import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -56,6 +57,7 @@ export interface WorkspacePanelProps { ExpressionRenderer: ReactExpressionRendererType; core: CoreStart | CoreSetup; plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; + title?: string; } export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel); @@ -73,6 +75,7 @@ export function InnerWorkspacePanel({ core, plugins, ExpressionRenderer: ExpressionRendererComponent, + title, }: WorkspacePanelProps) { const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); const emptyStateGraphicURL = IS_DARK_THEME @@ -291,13 +294,22 @@ export function InnerWorkspacePanel({ } return ( - - {renderVisualization()} - + + {renderVisualization()} + + ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.test.tsx new file mode 100644 index 0000000000000..517dff5b5e74c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Visualization } from '../../types'; +import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../mocks'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { WorkspacePanelWrapper, WorkspacePanelWrapperProps } from './workspace_panel_wrapper'; + +describe('workspace_panel_wrapper', () => { + let mockVisualization: jest.Mocked; + let mockFrameAPI: FrameMock; + let instance: ReactWrapper; + + beforeEach(() => { + mockVisualization = createMockVisualization(); + mockFrameAPI = createMockFramePublicAPI(); + }); + + afterEach(() => { + instance.unmount(); + }); + + it('should render its children', () => { + const MyChild = () => The child elements; + instance = mount( + + + + ); + + expect(instance.find(MyChild)).toHaveLength(1); + }); + + it('should call the toolbar renderer if provided', () => { + const renderToolbarMock = jest.fn(); + const visState = { internalState: 123 }; + instance = mount( + } + activeVisualization={{ ...mockVisualization, renderToolbar: renderToolbarMock }} + emptyExpression={false} + /> + ); + + expect(renderToolbarMock).toHaveBeenCalledWith(expect.any(Element), { + state: visState, + frame: mockFrameAPI, + setState: expect.anything(), + }); + }); +}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx index cc91510146f35..17461b9fc274f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx @@ -4,25 +4,86 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiPageContent, EuiPageContentHeader, EuiPageContentBody } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import classNames from 'classnames'; +import { + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { FramePublicAPI, Visualization } from '../../types'; +import { NativeRenderer } from '../../native_renderer'; +import { Action } from './state_management'; -interface Props { - title: string; +export interface WorkspacePanelWrapperProps { children: React.ReactNode | React.ReactNode[]; + framePublicAPI: FramePublicAPI; + visualizationState: unknown; + activeVisualization: Visualization | null; + dispatch: (action: Action) => void; + emptyExpression: boolean; + title?: string; } -export function WorkspacePanelWrapper({ children, title }: Props) { +export function WorkspacePanelWrapper({ + children, + framePublicAPI, + visualizationState, + activeVisualization, + dispatch, + title, + emptyExpression, +}: WorkspacePanelWrapperProps) { + const setVisualizationState = useCallback( + (newState: unknown) => { + if (!activeVisualization) { + return; + } + dispatch({ + type: 'UPDATE_VISUALIZATION_STATE', + visualizationId: activeVisualization.id, + newState, + clearStagedPreview: false, + }); + }, + [dispatch] + ); return ( - - {title && ( - - {title} - + + {activeVisualization && activeVisualization.renderToolbar && ( + + + )} - - {children} - - + + + {(!emptyExpression || title) && ( + + + {title || + i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} + + + )} + + {children} + + + + ); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss index 41919b900c71f..6e51c45ad02c1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss @@ -29,6 +29,7 @@ .lnsFieldItem__name { margin-left: $euiSizeS; flex-grow: 1; + word-break: break-word; } .lnsFieldListPanel__fieldIcon, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index c2437aa3cc3cc..d451e312446bd 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -290,6 +290,12 @@ export type VisualizationLayerWidgetProps = VisualizationConfigProp setState: (newState: T) => void; }; +export interface VisualizationToolbarProps { + setState: (newState: T) => void; + frame: FramePublicAPI; + state: T; +} + export type VisualizationDimensionEditorProps = VisualizationConfigProps & { groupId: string; accessor: string; @@ -454,6 +460,11 @@ export interface Visualization { * for extra configurability, such as for styling the legend or axis */ renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; + /** + * Toolbar rendered above the visualization. This is meant to be used to provide chart-level + * settings for the visualization. + */ + renderToolbar?: (domElement: Element, props: VisualizationToolbarProps) => void; /** * Visualizations can provide a custom icon which will open a layer-specific popover * If no icon is provided, gear icon is default diff --git a/x-pack/plugins/license_management/public/application/app_context.tsx b/x-pack/plugins/license_management/public/application/app_context.tsx index 39e7ef5f16e79..62f019682fba9 100644 --- a/x-pack/plugins/license_management/public/application/app_context.tsx +++ b/x-pack/plugins/license_management/public/application/app_context.tsx @@ -9,7 +9,7 @@ import { ScopedHistory } from 'kibana/public'; import { CoreStart } from '../../../../../src/core/public'; import { LicensingPluginSetup, ILicense } from '../../../licensing/public'; -import { TelemetryPluginSetup } from '../../../../../src/plugins/telemetry/public'; +import { TelemetryPluginStart } from '../../../../../src/plugins/telemetry/public'; import { ClientConfigType } from '../types'; import { BreadcrumbService } from './breadcrumbs'; @@ -23,7 +23,7 @@ export interface AppDependencies { }; plugins: { licensing: LicensingPluginSetup; - telemetry?: TelemetryPluginSetup; + telemetry?: TelemetryPluginStart; }; docLinks: { security: string; diff --git a/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/opt_in_example_flyout.tsx b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/opt_in_example_flyout.tsx new file mode 100644 index 0000000000000..a13443ad8a0d7 --- /dev/null +++ b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/opt_in_example_flyout.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OptInExampleFlyout } from '../../../../../../../src/plugins/telemetry_management_section/public'; + +// required for lazy loading +// eslint-disable-next-line import/no-default-export +export default OptInExampleFlyout; diff --git a/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx index eff5c6cc21c43..92e241a375cea 100644 --- a/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx +++ b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx @@ -5,13 +5,19 @@ */ import React, { Fragment } from 'react'; -import { EuiLink, EuiCheckbox, EuiSpacer, EuiText, EuiTitle, EuiPopover } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { - OptInExampleFlyout, - PRIVACY_STATEMENT_URL, - TelemetryPluginSetup, -} from '../../lib/telemetry'; + EuiLink, + EuiCheckbox, + EuiSpacer, + EuiText, + EuiTitle, + EuiPopover, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TelemetryPluginStart } from '../../lib/telemetry'; + +const OptInExampleFlyout = React.lazy(() => import('./opt_in_example_flyout')); interface State { showMoreTelemetryInfo: boolean; @@ -22,7 +28,7 @@ interface Props { onOptInChange: (isOptingInToTelemetry: boolean) => void; isOptingInToTelemetry: boolean; isStartTrial: boolean; - telemetry: TelemetryPluginSetup; + telemetry: TelemetryPluginStart; } export class TelemetryOptIn extends React.Component { @@ -54,11 +60,15 @@ export class TelemetryOptIn extends React.Component { let example = null; if (showExample) { + // Using React.Suspense and lazy loading here to avoid crashing the plugin when importing + // OptInExampleFlyout but telemetryManagementSection is disabled example = ( - this.setState({ showExample: false })} - fetchExample={telemetry.telemetryService.fetchExample} - /> + }> + this.setState({ showExample: false })} + fetchExample={telemetry.telemetryService.fetchExample} + /> + ); } @@ -116,7 +126,10 @@ export class TelemetryOptIn extends React.Component { ), telemetryPrivacyStatementLink: ( - + void; startLicenseTrial: () => void; - telemetry?: TelemetryPluginSetup; + telemetry?: TelemetryPluginStart; shouldShowStartTrial: boolean; } diff --git a/x-pack/plugins/license_management/public/plugin.ts b/x-pack/plugins/license_management/public/plugin.ts index e2e6437d12d2a..2511337793fea 100644 --- a/x-pack/plugins/license_management/public/plugin.ts +++ b/x-pack/plugins/license_management/public/plugin.ts @@ -6,7 +6,7 @@ import { first } from 'rxjs/operators'; import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; -import { TelemetryPluginSetup } from '../../../../src/plugins/telemetry/public'; +import { TelemetryPluginStart } from '../../../../src/plugins/telemetry/public'; import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; import { LicensingPluginSetup } from '../../../plugins/licensing/public'; import { PLUGIN } from '../common/constants'; @@ -14,10 +14,13 @@ import { ClientConfigType } from './types'; import { AppDependencies } from './application'; import { BreadcrumbService } from './application/breadcrumbs'; -interface PluginsDependencies { +interface PluginsDependenciesSetup { management: ManagementSetup; licensing: LicensingPluginSetup; - telemetry?: TelemetryPluginSetup; +} + +interface PluginsDependenciesStart { + telemetry?: TelemetryPluginStart; } export interface LicenseManagementUIPluginSetup { @@ -31,7 +34,10 @@ export class LicenseManagementUIPlugin constructor(private readonly initializerContext: PluginInitializerContext) {} - setup(coreSetup: CoreSetup, plugins: PluginsDependencies): LicenseManagementUIPluginSetup { + setup( + coreSetup: CoreSetup, + plugins: PluginsDependenciesSetup + ): LicenseManagementUIPluginSetup { const config = this.initializerContext.config.get(); if (!config.ui.enabled) { @@ -42,14 +48,14 @@ export class LicenseManagementUIPlugin } const { getStartServices } = coreSetup; - const { management, telemetry, licensing } = plugins; + const { management, licensing } = plugins; management.sections.getSection(ManagementSectionId.Stack).registerApp({ id: PLUGIN.id, title: PLUGIN.title, order: 0, mount: async ({ element, setBreadcrumbs, history }) => { - const [core] = await getStartServices(); + const [core, { telemetry }] = await getStartServices(); const initialLicense = await plugins.licensing.license$.pipe(first()).toPromise(); // Setup documentation links diff --git a/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts b/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts index 29bff40293958..4942d21f64ee2 100644 --- a/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts +++ b/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts @@ -5,9 +5,19 @@ */ import { BehaviorSubject } from 'rxjs'; -import { licenseMock } from '../common/licensing.mock'; +import { licenseMock } from '../common/licensing.mock'; import { createRouteHandlerContext } from './licensing_route_handler_context'; +import { featureUsageMock } from './services/feature_usage_service.mock'; +import { FeatureUsageServiceStart } from './services'; +import { StartServicesAccessor } from 'src/core/server'; +import { LicensingPluginStart } from './types'; + +const createStartServices = ( + featureUsage: FeatureUsageServiceStart = featureUsageMock.createStart() +): StartServicesAccessor<{}, LicensingPluginStart> => { + return async () => [{} as any, {}, { featureUsage } as LicensingPluginStart]; +}; describe('createRouteHandlerContext', () => { it('returns a function providing the last license value', async () => { @@ -15,7 +25,7 @@ describe('createRouteHandlerContext', () => { const secondLicense = licenseMock.createLicense(); const license$ = new BehaviorSubject(firstLicense); - const routeHandler = createRouteHandlerContext(license$); + const routeHandler = createRouteHandlerContext(license$, createStartServices()); const firstCtx = await routeHandler({} as any, {} as any, {} as any); license$.next(secondLicense); @@ -24,4 +34,14 @@ describe('createRouteHandlerContext', () => { expect(firstCtx.license).toBe(firstLicense); expect(secondCtx.license).toBe(secondLicense); }); + + it('returns a the feature usage API', async () => { + const license$ = new BehaviorSubject(licenseMock.createLicense()); + const featureUsage = featureUsageMock.createStart(); + + const routeHandler = createRouteHandlerContext(license$, createStartServices(featureUsage)); + const ctx = await routeHandler({} as any, {} as any, {} as any); + + expect(ctx.featureUsage).toBe(featureUsage); + }); }); diff --git a/x-pack/plugins/licensing/server/licensing_route_handler_context.ts b/x-pack/plugins/licensing/server/licensing_route_handler_context.ts index 42cb0959fc373..736a2151a3dbd 100644 --- a/x-pack/plugins/licensing/server/licensing_route_handler_context.ts +++ b/x-pack/plugins/licensing/server/licensing_route_handler_context.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IContextProvider, RequestHandler } from 'src/core/server'; +import { IContextProvider, RequestHandler, StartServicesAccessor } from 'src/core/server'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { ILicense } from '../common/types'; +import { LicensingPluginStart } from './types'; /** * Create a route handler context for access to Kibana license information. @@ -16,9 +17,16 @@ import { ILicense } from '../common/types'; * @public */ export function createRouteHandlerContext( - license$: Observable + license$: Observable, + getStartServices: StartServicesAccessor<{}, LicensingPluginStart> ): IContextProvider, 'licensing'> { return async function licensingRouteHandlerContext() { - return { license: await license$.pipe(take(1)).toPromise() }; + const [, , { featureUsage }] = await getStartServices(); + const license = await license$.pipe(take(1)).toPromise(); + + return { + featureUsage, + license, + }; }; } diff --git a/x-pack/plugins/licensing/server/mocks.ts b/x-pack/plugins/licensing/server/mocks.ts index 0d154f76d5134..1a2b543b47df5 100644 --- a/x-pack/plugins/licensing/server/mocks.ts +++ b/x-pack/plugins/licensing/server/mocks.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { BehaviorSubject } from 'rxjs'; -import { LicensingPluginSetup, LicensingPluginStart } from './types'; +import { + LicensingPluginSetup, + LicensingPluginStart, + LicensingRequestHandlerContext, +} from './types'; import { licenseMock } from '../common/licensing.mock'; import { featureUsageMock } from './services/feature_usage_service.mock'; @@ -43,8 +47,20 @@ const createStartMock = (): jest.Mocked => { return mock; }; +const createRequestHandlerContextMock = ( + ...options: Parameters +): jest.Mocked => { + const mock: jest.Mocked = { + license: licenseMock.createLicense(...options), + featureUsage: featureUsageMock.createStart(), + }; + + return mock; +}; + export const licensingMock = { createSetup: createSetupMock, createStart: createStartMock, + createRequestHandlerContext: createRequestHandlerContextMock, ...licenseMock, }; diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index e1aa4a1b32517..0a6964b1b829d 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -128,7 +128,10 @@ export class LicensingPlugin implements Plugin { - return [VECTOR_SHAPE_TYPES.POLYGON]; + async getSupportedShapeTypes(): Promise { + return [VECTOR_SHAPE_TYPE.POLYGON]; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js index c05c1f2dd7c1e..b613f577067ba 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js @@ -7,7 +7,6 @@ import React from 'react'; import uuid from 'uuid/v4'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson'; import { UpdateSourceEditor } from './update_source_editor'; import { @@ -15,6 +14,7 @@ import { DEFAULT_MAX_BUCKETS_LIMIT, RENDER_AS, GRID_RESOLUTION, + VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -326,10 +326,10 @@ export class ESGeoGridSource extends AbstractESAggSource { async getSupportedShapeTypes() { if (this._descriptor.requestType === RENDER_AS.GRID) { - return [VECTOR_SHAPE_TYPES.POLYGON]; + return [VECTOR_SHAPE_TYPE.POLYGON]; } - return [VECTOR_SHAPE_TYPES.POINT]; + return [VECTOR_SHAPE_TYPE.POINT]; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index fda73bc0f73a0..076e7a758a4fb 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -7,10 +7,9 @@ import React from 'react'; import uuid from 'uuid/v4'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; -import { SOURCE_TYPES } from '../../../../common/constants'; +import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { convertToLines } from './convert_to_lines'; import { AbstractESAggSource } from '../es_agg_source'; @@ -61,7 +60,7 @@ export class ESPewPewSource extends AbstractESAggSource { } async getSupportedShapeTypes() { - return [VECTOR_SHAPE_TYPES.LINE]; + return [VECTOR_SHAPE_TYPE.LINE]; } async getImmutableProperties() { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js index 51dd57ffad0d1..c8f14f1dc6a4b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js @@ -7,7 +7,6 @@ import _ from 'lodash'; import React from 'react'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { AbstractESSource } from '../es_source'; import { getSearchService } from '../../../kibana_services'; import { hitsToGeoJson } from '../../../elasticsearch_geo_utils'; @@ -18,6 +17,7 @@ import { DEFAULT_MAX_BUCKETS_LIMIT, SORT_ORDER, SCALING_TYPES, + VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -471,10 +471,10 @@ export class ESSearchSource extends AbstractESSource { } if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - return [VECTOR_SHAPE_TYPES.POINT]; + return [VECTOR_SHAPE_TYPE.POINT]; } - return [VECTOR_SHAPE_TYPES.POINT, VECTOR_SHAPE_TYPES.LINE, VECTOR_SHAPE_TYPES.POLYGON]; + return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } getSourceTooltipContent(sourceDataRequest) { diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts index 86a1589a7a030..03b91df22d3ca 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -8,8 +8,7 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { AbstractSource, ImmutableSourceProperty } from '../source'; import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; -import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES } from '../../../../common/constants'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; +import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { IField } from '../../fields/field'; import { registerSource } from '../source_registry'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; @@ -116,8 +115,8 @@ export class MVTSingleLayerVectorSource extends AbstractSource }; } - async getSupportedShapeTypes(): Promise { - return [VECTOR_SHAPE_TYPES.POINT, VECTOR_SHAPE_TYPES.LINE, VECTOR_SHAPE_TYPES.POLYGON]; + async getSupportedShapeTypes(): Promise { + return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } canFormatFeatureProperties() { diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts index 711b7d600d74d..99a7478cd8362 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts @@ -16,7 +16,7 @@ import { VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; +import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; @@ -68,7 +68,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc getFields(): Promise; getFieldByName(fieldName: string): IField | null; getSyncMeta(): VectorSourceSyncMeta; - getSupportedShapeTypes(): Promise; + getSupportedShapeTypes(): Promise; canFormatFeatureProperties(): boolean; getApplyGlobalQuery(): boolean; getFieldNames(): string[]; diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js index ccf6c7963c9b4..ecb13bb875721 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js @@ -9,7 +9,7 @@ import { AbstractSource } from './../source'; import * as topojson from 'topojson-client'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { VECTOR_SHAPE_TYPES } from './../vector_feature_types'; +import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; export class AbstractVectorSource extends AbstractSource { static async getGeoJson({ format, featureCollectionPath, fetchUrl }) { @@ -127,7 +127,7 @@ export class AbstractVectorSource extends AbstractSource { } async getSupportedShapeTypes() { - return [VECTOR_SHAPE_TYPES.POINT, VECTOR_SHAPE_TYPES.LINE, VECTOR_SHAPE_TYPES.POLYGON]; + return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } getSourceTooltipContent(/* sourceDataRequest */) { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js index 3424a972fed06..7856a4ddaff39 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js @@ -16,7 +16,6 @@ import { VectorStyleLabelBorderSizeEditor } from './label/vector_style_label_bor import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../vector_style_defaults'; import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_utils'; -import { VECTOR_SHAPE_TYPES } from '../../../sources/vector_feature_types'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch } from '@elastic/eui'; @@ -26,6 +25,7 @@ import { LABEL_BORDER_SIZES, VECTOR_STYLES, STYLE_TYPE, + VECTOR_SHAPE_TYPE, } from '../../../../../common/constants'; export class VectorStyleEditor extends Component { @@ -96,11 +96,11 @@ export class VectorStyleEditor extends Component { } if (this.state.selectedFeature === null) { - let selectedFeature = VECTOR_SHAPE_TYPES.POLYGON; + let selectedFeature = VECTOR_SHAPE_TYPE.POLYGON; if (this.props.isPointsOnly) { - selectedFeature = VECTOR_SHAPE_TYPES.POINT; + selectedFeature = VECTOR_SHAPE_TYPE.POINT; } else if (this.props.isLinesOnly) { - selectedFeature = VECTOR_SHAPE_TYPES.LINE; + selectedFeature = VECTOR_SHAPE_TYPE.LINE; } this.setState({ selectedFeature: selectedFeature, @@ -414,30 +414,30 @@ export class VectorStyleEditor extends Component { if (supportedFeatures.length === 1) { switch (supportedFeatures[0]) { - case VECTOR_SHAPE_TYPES.POINT: + case VECTOR_SHAPE_TYPE.POINT: return this._renderPointProperties(); - case VECTOR_SHAPE_TYPES.LINE: + case VECTOR_SHAPE_TYPE.LINE: return this._renderLineProperties(); - case VECTOR_SHAPE_TYPES.POLYGON: + case VECTOR_SHAPE_TYPE.POLYGON: return this._renderPolygonProperties(); } } const featureButtons = [ { - id: VECTOR_SHAPE_TYPES.POINT, + id: VECTOR_SHAPE_TYPE.POINT, label: i18n.translate('xpack.maps.vectorStyleEditor.pointLabel', { defaultMessage: 'Points', }), }, { - id: VECTOR_SHAPE_TYPES.LINE, + id: VECTOR_SHAPE_TYPE.LINE, label: i18n.translate('xpack.maps.vectorStyleEditor.lineLabel', { defaultMessage: 'Lines', }), }, { - id: VECTOR_SHAPE_TYPES.POLYGON, + id: VECTOR_SHAPE_TYPE.POLYGON, label: i18n.translate('xpack.maps.vectorStyleEditor.polygonLabel', { defaultMessage: 'Polygons', }), @@ -445,9 +445,9 @@ export class VectorStyleEditor extends Component { ]; let styleProperties = this._renderPolygonProperties(); - if (selectedFeature === VECTOR_SHAPE_TYPES.LINE) { + if (selectedFeature === VECTOR_SHAPE_TYPE.LINE) { styleProperties = this._renderLineProperties(); - } else if (selectedFeature === VECTOR_SHAPE_TYPES.POINT) { + } else if (selectedFeature === VECTOR_SHAPE_TYPE.POINT) { styleProperties = this._renderPointProperties(); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap index b4843324a0def..631a6117a111d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Should render categorical legend with breaks 1`] = ` +exports[`renderLegendDetailRow Should render categorical legend with breaks 1`] = `
+ + + Other + + } + styleName="icon" + symbolId="square" + /> +
`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js index 4c02dee762e9d..556bb2b79e836 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js @@ -13,7 +13,7 @@ import { GRADIENT_INTERVALS, } from '../../color_utils'; import React from 'react'; -import { COLOR_MAP_TYPE } from '../../../../../common/constants'; +import { COLOR_MAP_TYPE, MB_LOOKUP_FUNCTION } from '../../../../../common/constants'; import { isCategoricalStopsInvalid, getOtherCategoryLabel, @@ -152,7 +152,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { makeMbClampedNumberExpression({ minValue: rangeFieldMeta.min, maxValue: rangeFieldMeta.max, - lookupFunction: 'feature-state', + lookupFunction: MB_LOOKUP_FUNCTION.FEATURE_STATE, fallback: lessThanFirstStopValue, fieldName: targetName, }), diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js index c7620512710dc..665317569e5e8 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js @@ -23,7 +23,7 @@ export class DynamicIconProperty extends DynamicStyleProperty { getNumberOfCategories() { const palette = getIconPalette(this._options.iconPaletteId); - return palette ? palette.length : 0; + return palette.length; } syncIconWithMb(symbolLayerId, mbMap, iconPixelSize) { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index 505c08ac35ba7..132c0b3f27603 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -34,8 +34,8 @@ const makeProperty = (options: Partial, field: IField = mock ); }; -describe('DynamicIconProperty', () => { - it('should derive category number from palettes', async () => { +describe('getNumberOfCategories', () => { + test('should derive category number from palettes', async () => { const filled = makeProperty({ iconPaletteId: 'filledShapes', }); @@ -47,15 +47,53 @@ describe('DynamicIconProperty', () => { }); }); -test('Should render categorical legend with breaks', async () => { - const iconStyle = makeProperty({ - iconPaletteId: 'filledShapes', +describe('renderLegendDetailRow', () => { + test('Should render categorical legend with breaks', async () => { + const iconStyle = makeProperty({ + iconPaletteId: 'filledShapes', + }); + + const legendRow = iconStyle.renderLegendDetailRow({ isPointsOnly: true, isLinesOnly: false }); + const component = shallow(legendRow); + await new Promise((resolve) => process.nextTick(resolve)); + component.update(); + + expect(component).toMatchSnapshot(); }); +}); - const legendRow = iconStyle.renderLegendDetailRow({ isPointsOnly: true, isLinesOnly: false }); - const component = shallow(legendRow); - await new Promise((resolve) => process.nextTick(resolve)); - component.update(); +describe('get mapbox icon-image expression (via internal _getMbIconImageExpression)', () => { + describe('categorical icon palette', () => { + test('should return mapbox expression for pre-defined icon palette', async () => { + const iconStyle = makeProperty({ + iconPaletteId: 'filledShapes', + }); + expect(iconStyle._getMbIconImageExpression(15)).toEqual([ + 'match', + ['to-string', ['get', 'foobar']], + 'US', + 'circle-15', + 'CN', + 'marker-15', + 'square-15', + ]); + }); - expect(component).toMatchSnapshot(); + test('should return mapbox expression for custom icon palette', async () => { + const iconStyle = makeProperty({ + useCustomIconMap: true, + customIconStops: [ + { stop: null, icon: 'circle' }, + { stop: 'MX', icon: 'marker' }, + ], + }); + expect(iconStyle._getMbIconImageExpression(15)).toEqual([ + 'match', + ['to-string', ['get', 'foobar']], + 'MX', + 'marker-15', + 'circle-15', + ]); + }); + }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js index a0af2fbb939d8..662d1ccf33b95 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js @@ -12,7 +12,7 @@ import { LARGE_MAKI_ICON_SIZE, SMALL_MAKI_ICON_SIZE, } from '../symbol_utils'; -import { VECTOR_STYLES } from '../../../../../common/constants'; +import { MB_LOOKUP_FUNCTION, VECTOR_STYLES } from '../../../../../common/constants'; import _ from 'lodash'; import React from 'react'; @@ -60,7 +60,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { minValue: rangeFieldMeta.min, maxValue: rangeFieldMeta.max, fallback: 0, - lookupFunction: 'get', + lookupFunction: MB_LOOKUP_FUNCTION.GET, fieldName: targetName, }), rangeFieldMeta.min, @@ -109,7 +109,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } _getMbDataDrivenSize({ targetName, minSize, maxSize, minValue, maxValue }) { - const lookup = this.supportsMbFeatureState() ? 'feature-state' : 'get'; + const lookup = this.supportsMbFeatureState() + ? MB_LOOKUP_FUNCTION.FEATURE_STATE + : MB_LOOKUP_FUNCTION.GET; const stops = minValue === maxValue ? [maxValue, maxSize] : [minValue, minSize, maxValue, maxSize]; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.js b/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts similarity index 60% rename from x-pack/plugins/maps/public/classes/styles/vector/style_util.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts index eb4c6708fb2dd..6c1f060383d05 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts @@ -5,58 +5,67 @@ */ import { isOnlySingleFeatureType, assignCategoriesToPalette, dynamicRound } from './style_util'; -import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; +import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; describe('isOnlySingleFeatureType', () => { describe('source supports single feature type', () => { - const supportedFeatures = [VECTOR_SHAPE_TYPES.POINT]; + const supportedFeatures = [VECTOR_SHAPE_TYPE.POINT]; + const hasFeatureType = { + [VECTOR_SHAPE_TYPE.POINT]: false, + [VECTOR_SHAPE_TYPE.LINE]: false, + [VECTOR_SHAPE_TYPE.POLYGON]: false, + }; test('Is only single feature type when only supported feature type is target feature type', () => { - expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures)).toBe(true); + expect( + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.POINT, supportedFeatures, hasFeatureType) + ).toBe(true); }); test('Is not single feature type when only supported feature type is not target feature type', () => { - expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE, supportedFeatures)).toBe(false); + expect( + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.LINE, supportedFeatures, hasFeatureType) + ).toBe(false); }); }); describe('source supports multiple feature types', () => { const supportedFeatures = [ - VECTOR_SHAPE_TYPES.POINT, - VECTOR_SHAPE_TYPES.LINE, - VECTOR_SHAPE_TYPES.POLYGON, + VECTOR_SHAPE_TYPE.POINT, + VECTOR_SHAPE_TYPE.LINE, + VECTOR_SHAPE_TYPE.POLYGON, ]; test('Is only single feature type when data only has target feature type', () => { const hasFeatureType = { - [VECTOR_SHAPE_TYPES.POINT]: true, - [VECTOR_SHAPE_TYPES.LINE]: false, - [VECTOR_SHAPE_TYPES.POLYGON]: false, + [VECTOR_SHAPE_TYPE.POINT]: true, + [VECTOR_SHAPE_TYPE.LINE]: false, + [VECTOR_SHAPE_TYPE.POLYGON]: false, }; expect( - isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures, hasFeatureType) + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.POINT, supportedFeatures, hasFeatureType) ).toBe(true); }); test('Is not single feature type when data has multiple feature types', () => { const hasFeatureType = { - [VECTOR_SHAPE_TYPES.POINT]: true, - [VECTOR_SHAPE_TYPES.LINE]: true, - [VECTOR_SHAPE_TYPES.POLYGON]: true, + [VECTOR_SHAPE_TYPE.POINT]: true, + [VECTOR_SHAPE_TYPE.LINE]: true, + [VECTOR_SHAPE_TYPE.POLYGON]: true, }; expect( - isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE, supportedFeatures, hasFeatureType) + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.LINE, supportedFeatures, hasFeatureType) ).toBe(false); }); test('Is not single feature type when data does not have target feature types', () => { const hasFeatureType = { - [VECTOR_SHAPE_TYPES.POINT]: false, - [VECTOR_SHAPE_TYPES.LINE]: true, - [VECTOR_SHAPE_TYPES.POLYGON]: false, + [VECTOR_SHAPE_TYPE.POINT]: false, + [VECTOR_SHAPE_TYPE.LINE]: true, + [VECTOR_SHAPE_TYPE.POLYGON]: false, }; expect( - isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures, hasFeatureType) + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.POINT, supportedFeatures, hasFeatureType) ).toBe(false); }); }); @@ -64,7 +73,12 @@ describe('isOnlySingleFeatureType', () => { describe('assignCategoriesToPalette', () => { test('Categories and palette values have same length', () => { - const categories = [{ key: 'alpah' }, { key: 'bravo' }, { key: 'charlie' }, { key: 'delta' }]; + const categories = [ + { key: 'alpah', count: 1 }, + { key: 'bravo', count: 1 }, + { key: 'charlie', count: 1 }, + { key: 'delta', count: 1 }, + ]; const paletteValues = ['red', 'orange', 'yellow', 'green']; expect(assignCategoriesToPalette({ categories, paletteValues })).toEqual({ stops: [ @@ -72,31 +86,39 @@ describe('assignCategoriesToPalette', () => { { stop: 'bravo', style: 'orange' }, { stop: 'charlie', style: 'yellow' }, ], - fallback: 'green', + fallbackSymbolId: 'green', }); }); test('Should More categories than palette values', () => { - const categories = [{ key: 'alpah' }, { key: 'bravo' }, { key: 'charlie' }, { key: 'delta' }]; + const categories = [ + { key: 'alpah', count: 1 }, + { key: 'bravo', count: 1 }, + { key: 'charlie', count: 1 }, + { key: 'delta', count: 1 }, + ]; const paletteValues = ['red', 'orange', 'yellow']; expect(assignCategoriesToPalette({ categories, paletteValues })).toEqual({ stops: [ { stop: 'alpah', style: 'red' }, { stop: 'bravo', style: 'orange' }, ], - fallback: 'yellow', + fallbackSymbolId: 'yellow', }); }); test('Less categories than palette values', () => { - const categories = [{ key: 'alpah' }, { key: 'bravo' }]; + const categories = [ + { key: 'alpah', count: 1 }, + { key: 'bravo', count: 1 }, + ]; const paletteValues = ['red', 'orange', 'yellow', 'green', 'blue']; expect(assignCategoriesToPalette({ categories, paletteValues })).toEqual({ stops: [ { stop: 'alpah', style: 'red' }, { stop: 'bravo', style: 'orange' }, ], - fallback: 'yellow', + fallbackSymbolId: 'yellow', }); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_util.js b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts similarity index 57% rename from x-pack/plugins/maps/public/classes/styles/vector/style_util.js rename to x-pack/plugins/maps/public/classes/styles/vector/style_util.ts index 3b62dcb27dced..d190a62e6f300 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_util.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts @@ -5,6 +5,8 @@ */ import { i18n } from '@kbn/i18n'; +import { MB_LOOKUP_FUNCTION, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; +import { Category } from '../../../../common/descriptor_types'; export function getOtherCategoryLabel() { return i18n.translate('xpack.maps.styles.categorical.otherCategoryLabel', { @@ -12,29 +14,32 @@ export function getOtherCategoryLabel() { }); } -export function getComputedFieldName(styleName, fieldName) { +export function getComputedFieldName(styleName: string, fieldName: string) { return `${getComputedFieldNamePrefix(fieldName)}__${styleName}`; } -export function getComputedFieldNamePrefix(fieldName) { +export function getComputedFieldNamePrefix(fieldName: string) { return `__kbn__dynamic__${fieldName}`; } -export function isOnlySingleFeatureType(featureType, supportedFeatures, hasFeatureType) { +export function isOnlySingleFeatureType( + featureType: VECTOR_SHAPE_TYPE, + supportedFeatures: VECTOR_SHAPE_TYPE[], + hasFeatureType: { [key in keyof typeof VECTOR_SHAPE_TYPE]: boolean } +): boolean { if (supportedFeatures.length === 1) { return supportedFeatures[0] === featureType; } const featureTypes = Object.keys(hasFeatureType); - return featureTypes.reduce((isOnlyTargetFeatureType, featureTypeKey) => { + // @ts-expect-error + return featureTypes.reduce((accumulator: boolean, featureTypeKey: VECTOR_SHAPE_TYPE) => { const hasFeature = hasFeatureType[featureTypeKey]; - return featureTypeKey === featureType - ? isOnlyTargetFeatureType && hasFeature - : isOnlyTargetFeatureType && !hasFeature; + return featureTypeKey === featureType ? accumulator && hasFeature : accumulator && !hasFeature; }, true); } -export function dynamicRound(value) { +export function dynamicRound(value: number | string) { if (typeof value !== 'number') { return value; } @@ -49,13 +54,19 @@ export function dynamicRound(value) { return precision === 0 ? Math.round(value) : parseFloat(value.toFixed(precision + 1)); } -export function assignCategoriesToPalette({ categories, paletteValues }) { +export function assignCategoriesToPalette({ + categories, + paletteValues, +}: { + categories: Category[]; + paletteValues: string[]; +}) { const stops = []; - let fallback = null; + let fallbackSymbolId = null; - if (categories && categories.length && paletteValues && paletteValues.length) { + if (categories.length && paletteValues.length) { const maxLength = Math.min(paletteValues.length, categories.length + 1); - fallback = paletteValues[maxLength - 1]; + fallbackSymbolId = paletteValues[maxLength - 1]; for (let i = 0; i < maxLength - 1; i++) { stops.push({ stop: categories[i].key, @@ -66,7 +77,7 @@ export function assignCategoriesToPalette({ categories, paletteValues }) { return { stops, - fallback, + fallbackSymbolId, }; } @@ -76,6 +87,12 @@ export function makeMbClampedNumberExpression({ minValue, maxValue, fallback, +}: { + lookupFunction: MB_LOOKUP_FUNCTION; + fieldName: string; + minValue: number; + maxValue: number; + fallback: number; }) { const clamp = ['max', ['min', ['to-number', [lookupFunction, fieldName]], maxValue], minValue]; return [ @@ -83,7 +100,7 @@ export function makeMbClampedNumberExpression({ [ 'case', ['==', [lookupFunction, fieldName], null], - minValue - 1, //== does a JS-y like check where returns true for null and undefined + minValue - 1, // == does a JS-y like check where returns true for null and undefined clamp, ], fallback, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js index 1672af8eccff8..04df9d73d75cd 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js @@ -140,5 +140,5 @@ export function getIconPaletteOptions(isDarkMode) { export function getIconPalette(paletteId) { const palette = ICON_PALETTES.find(({ id }) => id === paletteId); - return palette ? [...palette.icons] : null; + return palette ? [...palette.icons] : []; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js index 989ac268c0552..04a5381fa2592 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js @@ -16,12 +16,12 @@ import { SOURCE_FORMATTERS_DATA_REQUEST_ID, LAYER_STYLE_TYPE, DEFAULT_ICON, + VECTOR_SHAPE_TYPE, VECTOR_STYLES, } from '../../../../common/constants'; import { StyleMeta } from './style_meta'; import { VectorIcon } from './components/legend/vector_icon'; import { VectorStyleLegend } from './components/legend/vector_style_legend'; -import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; import { getComputedFieldName, isOnlySingleFeatureType } from './style_util'; import { StaticStyleProperty } from './properties/static_style_property'; import { DynamicStyleProperty } from './properties/dynamic_style_property'; @@ -249,24 +249,24 @@ export class VectorStyle extends AbstractStyle { const supportedFeatures = await this._source.getSupportedShapeTypes(); const hasFeatureType = { - [VECTOR_SHAPE_TYPES.POINT]: false, - [VECTOR_SHAPE_TYPES.LINE]: false, - [VECTOR_SHAPE_TYPES.POLYGON]: false, + [VECTOR_SHAPE_TYPE.POINT]: false, + [VECTOR_SHAPE_TYPE.LINE]: false, + [VECTOR_SHAPE_TYPE.POLYGON]: false, }; if (supportedFeatures.length > 1) { for (let i = 0; i < features.length; i++) { const feature = features[i]; - if (!hasFeatureType[VECTOR_SHAPE_TYPES.POINT] && POINTS.includes(feature.geometry.type)) { - hasFeatureType[VECTOR_SHAPE_TYPES.POINT] = true; + if (!hasFeatureType[VECTOR_SHAPE_TYPE.POINT] && POINTS.includes(feature.geometry.type)) { + hasFeatureType[VECTOR_SHAPE_TYPE.POINT] = true; } - if (!hasFeatureType[VECTOR_SHAPE_TYPES.LINE] && LINES.includes(feature.geometry.type)) { - hasFeatureType[VECTOR_SHAPE_TYPES.LINE] = true; + if (!hasFeatureType[VECTOR_SHAPE_TYPE.LINE] && LINES.includes(feature.geometry.type)) { + hasFeatureType[VECTOR_SHAPE_TYPE.LINE] = true; } if ( - !hasFeatureType[VECTOR_SHAPE_TYPES.POLYGON] && + !hasFeatureType[VECTOR_SHAPE_TYPE.POLYGON] && POLYGONS.includes(feature.geometry.type) ) { - hasFeatureType[VECTOR_SHAPE_TYPES.POLYGON] = true; + hasFeatureType[VECTOR_SHAPE_TYPE.POLYGON] = true; } } } @@ -274,17 +274,17 @@ export class VectorStyle extends AbstractStyle { const styleMeta = { geometryTypes: { isPointsOnly: isOnlySingleFeatureType( - VECTOR_SHAPE_TYPES.POINT, + VECTOR_SHAPE_TYPE.POINT, supportedFeatures, hasFeatureType ), isLinesOnly: isOnlySingleFeatureType( - VECTOR_SHAPE_TYPES.LINE, + VECTOR_SHAPE_TYPE.LINE, supportedFeatures, hasFeatureType ), isPolygonsOnly: isOnlySingleFeatureType( - VECTOR_SHAPE_TYPES.POLYGON, + VECTOR_SHAPE_TYPE.POLYGON, supportedFeatures, hasFeatureType ), diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js index 426f1d6afa952..a0dc07b8e545b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js @@ -6,8 +6,7 @@ import { VectorStyle } from './vector_style'; import { DataRequest } from '../../util/data_request'; -import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; -import { FIELD_ORIGIN, STYLE_TYPE } from '../../../../common/constants'; +import { FIELD_ORIGIN, STYLE_TYPE, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; jest.mock('../../../kibana_services'); jest.mock('ui/new_platform'); @@ -28,7 +27,7 @@ class MockField { class MockSource { constructor({ supportedShapeTypes } = {}) { - this._supportedShapeTypes = supportedShapeTypes || Object.values(VECTOR_SHAPE_TYPES); + this._supportedShapeTypes = supportedShapeTypes || Object.values(VECTOR_SHAPE_TYPE); } getSupportedShapeTypes() { return this._supportedShapeTypes; diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index c73225fc4285b..8fb0ecb50b28b 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -12,7 +12,7 @@ import { IContainer, } from '../../../../../src/plugins/embeddable/public'; import '../index.scss'; -import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; +import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { LayerDescriptor } from '../../common/descriptor_types'; import { MapEmbeddableInput } from './types'; import { lazyLoadMapModules } from '../lazy_load_bundle'; @@ -113,7 +113,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { { layerList, title: savedMap.title, - editUrl: getHttp().basePath.prepend(createMapPath(savedObjectId)), + editUrl: getHttp().basePath.prepend(getExistingMapPath(savedObjectId)), indexPatterns, editable: await this.isEditable(), settings, diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.js b/x-pack/plugins/maps/public/maps_vis_type_alias.js index cb7b3db17eab5..d90674f0f7725 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.js +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.js @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { APP_ID, APP_ICON, MAP_PATH } from '../common/constants'; import { getShowMapVisualizationTypes, getVisualizations } from './kibana_services'; export function getMapsVisTypeAlias() { @@ -28,7 +28,7 @@ The Maps app offers more functionality and is easier to use.`, return { aliasApp: APP_ID, - aliasPath: `/${MAP_SAVED_OBJECT_TYPE}`, + aliasPath: `/${MAP_PATH}`, name: APP_ID, title: i18n.translate('xpack.maps.visTypeAlias.title', { defaultMessage: 'Maps', diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js index f24c7be65afa3..f8c783f673bab 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js @@ -19,7 +19,7 @@ import { import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_selectors'; import { copyPersistentState } from '../../../reducers/util'; import { extractReferences, injectReferences } from '../../../../common/migrations/references'; -import { MAP_BASE_URL, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; import { getStore } from '../../store_operations'; export function createSavedGisMapClass(services) { @@ -76,7 +76,7 @@ export function createSavedGisMapClass(services) { } getFullPath() { - return `${MAP_BASE_URL}/${this.id}`; + return getExistingMapPath(this.id); } getLayerList() { diff --git a/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js b/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js index 36a355719d945..de2ee42b49171 100644 --- a/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js +++ b/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { getCoreChrome } from '../../kibana_services'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { MAP_PATH } from '../../../common/constants'; import _ from 'lodash'; import { getLayerListRaw } from '../../selectors/map_selectors'; import { copyPersistentState } from '../../reducers/util'; @@ -31,7 +31,7 @@ function hasUnsavedChanges(savedMap, initialLayerListConfig) { } export const updateBreadcrumbs = (savedMap, initialLayerListConfig, currentPath = '') => { - const isOnMapNow = currentPath.startsWith(`/${MAP_SAVED_OBJECT_TYPE}`); + const isOnMapNow = currentPath.startsWith(`/${MAP_PATH}`); const breadCrumbs = isOnMapNow ? [ { diff --git a/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts b/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts index 383d7773663c6..f54776f5ab629 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts @@ -6,8 +6,6 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { getMapsTelemetry } from '../maps_telemetry'; -// @ts-ignore -import { TELEMETRY_TYPE } from '../../../common/constants'; import { MapsConfigType } from '../../../config'; export function registerMapsUsageCollector( @@ -19,7 +17,7 @@ export function registerMapsUsageCollector( } const mapsUsageCollector = usageCollection.makeUsageCollector({ - type: TELEMETRY_TYPE, + type: 'maps', isReady: () => true, fetch: async () => await getMapsTelemetry(config), }); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index f2331b9a1a960..60f3a9b68202c 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -14,8 +14,8 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; // @ts-ignore import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; -import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, createMapPath } from '../common/constants'; -import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; +import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../common/constants'; +import { mapSavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore import { setInternalRepository } from './kibana_server_services'; @@ -58,7 +58,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('ecommerce', [ { - path: createMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), + path: getExistingMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -80,7 +80,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('flights', [ { - path: createMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), + path: getExistingMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -101,7 +101,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addSavedObjectsToSampleDataset('logs', getWebLogsSavedObjects()); home.sampleData.addAppLinksToSampleDataset('logs', [ { - path: createMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), + path: getExistingMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -191,7 +191,6 @@ export class MapsPlugin implements Plugin { }, }); - core.savedObjects.registerType(mapsTelemetrySavedObjects); core.savedObjects.registerType(mapSavedObjects); registerMapsUsageCollector(usageCollection, currentConfig); diff --git a/x-pack/plugins/maps/server/saved_objects/index.ts b/x-pack/plugins/maps/server/saved_objects/index.ts index c4b779183a2de..804d720a13ab0 100644 --- a/x-pack/plugins/maps/server/saved_objects/index.ts +++ b/x-pack/plugins/maps/server/saved_objects/index.ts @@ -3,5 +3,4 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { mapsTelemetrySavedObjects } from './maps_telemetry'; export { mapSavedObjects } from './map'; diff --git a/x-pack/plugins/maps/server/saved_objects/map.ts b/x-pack/plugins/maps/server/saved_objects/map.ts index 0fcadc5a97203..ce9d579137864 100644 --- a/x-pack/plugins/maps/server/saved_objects/map.ts +++ b/x-pack/plugins/maps/server/saved_objects/map.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsType } from 'src/core/server'; -import { APP_ICON, createMapPath } from '../../common/constants'; +import { APP_ICON, getExistingMapPath } from '../../common/constants'; // @ts-ignore import { migrations } from './migrations'; @@ -31,7 +31,7 @@ export const mapSavedObjects: SavedObjectsType = { }, getInAppUrl(obj) { return { - path: createMapPath(obj.id), + path: getExistingMapPath(obj.id), uiCapabilitiesPath: 'maps.show', }; }, diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts deleted file mode 100644 index ad0b17af36dda..0000000000000 --- a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts +++ /dev/null @@ -1,46 +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 { SavedObjectsType } from 'src/core/server'; - -export const mapsTelemetrySavedObjects: SavedObjectsType = { - name: 'maps', - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - settings: { - properties: { - showMapVisualizationTypes: { type: 'boolean' }, - }, - }, - indexPatternsWithGeoFieldCount: { type: 'long' }, - indexPatternsWithGeoPointFieldCount: { type: 'long' }, - indexPatternsWithGeoShapeFieldCount: { type: 'long' }, - mapsTotalCount: { type: 'long' }, - timeCaptured: { type: 'date' }, - attributesPerMap: { - properties: { - dataSourcesCount: { - properties: { - min: { type: 'long' }, - max: { type: 'long' }, - avg: { type: 'long' }, - }, - }, - layersCount: { - properties: { - min: { type: 'long' }, - max: { type: 'long' }, - avg: { type: 'long' }, - }, - }, - layerTypesCount: { dynamic: 'true', properties: {} }, - emsVectorLayersCount: { dynamic: 'true', properties: {} }, - }, - }, - }, - }, -}; diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts index e96af89e52685..be15120cb19e1 100644 --- a/x-pack/plugins/maps/server/tutorials/ems/index.ts +++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { TutorialsCategory } from '../../../../../../src/plugins/home/server'; -import { MAP_BASE_URL } from '../../../common/constants'; +import { getNewMapPath } from '../../../common/constants'; export function emsBoundariesSpecProvider({ emsLandingPageUrl, @@ -64,7 +64,7 @@ Indexing EMS administrative boundaries in Elasticsearch allows for search on bou 2. Click `Add layer`, then select `Upload GeoJSON`.\n\ 3. Upload the GeoJSON file and click `Import file`.', values: { - newMapUrl: prependBasePath(MAP_BASE_URL), + newMapUrl: prependBasePath(getNewMapPath()), }, }), }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index c0b7d63e623ce..07442124959d0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -48,7 +48,7 @@ export const Page: FC = () => {

  void; } export interface ESQueueInstance { - addJob: (type: string, payload: unknown, options: object) => Job; registerWorker: ( pluginId: string, workerFn: GenericWorkerFn, @@ -37,26 +36,25 @@ type GenericWorkerFn = ( ...workerRestArgs: any[] ) => void | Promise; -export async function createQueueFactory( +export async function createQueueFactory( reporting: ReportingCore, + store: ReportingStore, logger: LevelLogger ): Promise { const config = reporting.getConfig(); - const queueIndexInterval = config.get('queue', 'indexInterval'); + + // esqueue-related const queueTimeout = config.get('queue', 'timeout'); - const queueIndex = config.get('index'); const isPollingEnabled = config.get('queue', 'pollEnabled'); - const elasticsearch = await reporting.getElasticsearchService(); + const elasticsearch = reporting.getElasticsearchService(); const queueOptions = { - interval: queueIndexInterval, timeout: queueTimeout, - dateSeparator: '.', client: elasticsearch.legacy.client, logger: createTaggedLogger(logger, ['esqueue', 'queue-worker']), }; - const queue: ESQueueInstance = new Esqueue(queueIndex, queueOptions); + const queue: ESQueueInstance = new Esqueue(store, queueOptions); if (isPollingEnabled) { // create workers to poll the index for idle jobs waiting to be claimed and executed diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index 625da90f3b4f2..d1554a03b9389 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -4,39 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EventEmitter } from 'events'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { ESQueueCreateJobFn } from '../../server/types'; import { ReportingCore } from '../core'; -// @ts-ignore -import { events as esqueueEvents } from './esqueue'; -import { LevelLogger } from './level_logger'; +import { LevelLogger } from './'; +import { ReportingStore, Report } from './store'; -interface ConfirmedJob { - id: string; - index: string; - _seq_no: number; - _primary_term: number; -} - -export type Job = EventEmitter & { - id: string; - toJSON: () => { - id: string; - }; -}; - -export type EnqueueJobFn = ( +export type EnqueueJobFn = ( exportTypeId: string, - jobParams: JobParamsType, + jobParams: unknown, user: AuthenticatedUser | null, context: RequestHandlerContext, request: KibanaRequest -) => Promise; +) => Promise; export function enqueueJobFactory( reporting: ReportingCore, + store: ReportingStore, parentLogger: LevelLogger ): EnqueueJobFn { const config = reporting.getConfig(); @@ -45,16 +30,16 @@ export function enqueueJobFactory( const maxAttempts = config.get('capture', 'maxAttempts'); const logger = parentLogger.clone(['queue-job']); - return async function enqueueJob( + return async function enqueueJob( exportTypeId: string, - jobParams: JobParamsType, + jobParams: unknown, user: AuthenticatedUser | null, context: RequestHandlerContext, request: KibanaRequest - ): Promise { - type ScheduleTaskFnType = ESQueueCreateJobFn; + ) { + type ScheduleTaskFnType = ESQueueCreateJobFn; + const username = user ? user.username : false; - const esqueue = await reporting.getEsqueue(); const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); if (exportType == null) { @@ -71,16 +56,6 @@ export function enqueueJobFactory( max_attempts: maxAttempts, }; - return new Promise((resolve, reject) => { - const job = esqueue.addJob(exportType.jobType, payload, options); - - job.on(esqueueEvents.EVENT_JOB_CREATED, (createdJob: ConfirmedJob) => { - if (createdJob.id === job.id) { - logger.info(`Successfully queued job: ${createdJob.id}`); - resolve(job); - } - }); - job.on(esqueueEvents.EVENT_JOB_CREATE_ERROR, reject); - }); + return await store.addReport(exportType.jobType, payload, options); }; } diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js deleted file mode 100644 index 691bd4f618a1c..0000000000000 --- a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { createIndex } from '../../helpers/create_index'; -import { ClientMock } from '../fixtures/legacy_elasticsearch'; -import { constants } from '../../constants'; - -describe('Create Index', function () { - describe('Does not exist', function () { - let client; - let createSpy; - - beforeEach(function () { - client = new ClientMock(); - createSpy = sinon.spy(client, 'callAsInternalUser').withArgs('indices.create'); - }); - - it('should return true', function () { - const indexName = 'test-index'; - const result = createIndex(client, indexName); - - return result.then((exists) => expect(exists).to.be(true)); - }); - - it('should create the index with mappings and default settings', function () { - const indexName = 'test-index'; - const settings = constants.DEFAULT_SETTING_INDEX_SETTINGS; - const result = createIndex(client, indexName); - - return result.then(function () { - const payload = createSpy.getCall(0).args[1]; - sinon.assert.callCount(createSpy, 1); - expect(payload).to.have.property('index', indexName); - expect(payload).to.have.property('body'); - expect(payload.body).to.have.property('settings'); - expect(payload.body.settings).to.eql(settings); - expect(payload.body).to.have.property('mappings'); - expect(payload.body.mappings).to.have.property('properties'); - }); - }); - - it('should create the index with custom settings', function () { - const indexName = 'test-index'; - const settings = { - ...constants.DEFAULT_SETTING_INDEX_SETTINGS, - auto_expand_replicas: false, - number_of_shards: 3000, - number_of_replicas: 1, - format: '3000', - }; - const result = createIndex(client, indexName, settings); - - return result.then(function () { - const payload = createSpy.getCall(0).args[1]; - sinon.assert.callCount(createSpy, 1); - expect(payload).to.have.property('index', indexName); - expect(payload).to.have.property('body'); - expect(payload.body).to.have.property('settings'); - expect(payload.body.settings).to.eql(settings); - expect(payload.body).to.have.property('mappings'); - expect(payload.body.mappings).to.have.property('properties'); - }); - }); - }); - - describe('Does exist', function () { - let client; - let createSpy; - - beforeEach(function () { - client = new ClientMock(); - sinon - .stub(client, 'callAsInternalUser') - .withArgs('indices.exists') - .callsFake(() => Promise.resolve(true)); - createSpy = client.callAsInternalUser.withArgs('indices.create'); - }); - - it('should return true', function () { - const indexName = 'test-index'; - const result = createIndex(client, indexName); - - return result.then((exists) => expect(exists).to.be(true)); - }); - - it('should not create the index', function () { - const indexName = 'test-index'; - const result = createIndex(client, indexName); - - return result.then(function () { - sinon.assert.callCount(createSpy, 0); - }); - }); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/index_timestamp.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/index_timestamp.js deleted file mode 100644 index 71dc8a363e429..0000000000000 --- a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/helpers/index_timestamp.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import moment from 'moment'; -import { constants } from '../../constants'; -import { indexTimestamp } from '../../helpers/index_timestamp'; - -const anchor = '2016-04-02T01:02:03.456'; // saturday - -describe('Index timestamp interval', function () { - describe('construction', function () { - it('should throw given an invalid interval', function () { - const init = () => indexTimestamp('bananas'); - expect(init).to.throwException(/invalid.+interval/i); - }); - }); - - describe('timestamps', function () { - let clock; - let separator; - - beforeEach(function () { - separator = constants.DEFAULT_SETTING_DATE_SEPARATOR; - clock = sinon.useFakeTimers(moment(anchor).valueOf()); - }); - - afterEach(function () { - clock.restore(); - }); - - describe('formats', function () { - it('should return the year', function () { - const timestamp = indexTimestamp('year'); - const str = `2016`; - expect(timestamp).to.equal(str); - }); - - it('should return the year and month', function () { - const timestamp = indexTimestamp('month'); - const str = `2016${separator}04`; - expect(timestamp).to.equal(str); - }); - - it('should return the year, month, and first day of the week', function () { - const timestamp = indexTimestamp('week'); - const str = `2016${separator}03${separator}27`; - expect(timestamp).to.equal(str); - }); - - it('should return the year, month, and day of the week', function () { - const timestamp = indexTimestamp('day'); - const str = `2016${separator}04${separator}02`; - expect(timestamp).to.equal(str); - }); - - it('should return the year, month, day and hour', function () { - const timestamp = indexTimestamp('hour'); - const str = `2016${separator}04${separator}02${separator}01`; - expect(timestamp).to.equal(str); - }); - - it('should return the year, month, day, hour and minute', function () { - const timestamp = indexTimestamp('minute'); - const str = `2016${separator}04${separator}02${separator}01${separator}02`; - expect(timestamp).to.equal(str); - }); - }); - - describe('date separator', function () { - it('should be customizable', function () { - const separators = ['-', '.', '_']; - separators.forEach((customSep) => { - const str = `2016${customSep}04${customSep}02${customSep}01${customSep}02`; - const timestamp = indexTimestamp('minute', customSep); - expect(timestamp).to.equal(str); - }); - }); - - it('should throw if a letter is used', function () { - const separators = ['a', 'B', 'YYYY']; - separators.forEach((customSep) => { - const fn = () => indexTimestamp('minute', customSep); - expect(fn).to.throwException(); - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/job.js b/x-pack/plugins/reporting/server/lib/esqueue/__tests__/job.js deleted file mode 100644 index 955eed8d65722..0000000000000 --- a/x-pack/plugins/reporting/server/lib/esqueue/__tests__/job.js +++ /dev/null @@ -1,420 +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 events from 'events'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import proxyquire from 'proxyquire'; -import { QueueMock } from './fixtures/queue'; -import { ClientMock } from './fixtures/legacy_elasticsearch'; -import { constants } from '../constants'; - -const createIndexMock = sinon.stub(); -const { Job } = proxyquire.noPreserveCache()('../job', { - './helpers/create_index': { createIndex: createIndexMock }, -}); - -const maxPriority = 20; -const minPriority = -20; -const defaultPriority = 10; -const defaultCreatedBy = false; - -function validateDoc(spy) { - sinon.assert.callCount(spy, 1); - const spyCall = spy.getCall(0); - return spyCall.args[1]; -} - -describe('Job Class', function () { - let mockQueue; - let client; - let index; - - let type; - let payload; - let options; - - beforeEach(function () { - createIndexMock.resetHistory(); - createIndexMock.returns(Promise.resolve('mock')); - index = 'test'; - - client = new ClientMock(); - mockQueue = new QueueMock(); - mockQueue.setClient(client); - }); - - it('should be an event emitter', function () { - const job = new Job(mockQueue, index, 'test', {}); - expect(job).to.be.an(events.EventEmitter); - }); - - describe('invalid construction', function () { - it('should throw with a missing type', function () { - const init = () => new Job(mockQueue, index); - expect(init).to.throwException(/type.+string/i); - }); - - it('should throw with an invalid type', function () { - const init = () => new Job(mockQueue, index, { 'not a string': true }); - expect(init).to.throwException(/type.+string/i); - }); - - it('should throw with an invalid payload', function () { - const init = () => new Job(mockQueue, index, 'type1', [1, 2, 3]); - expect(init).to.throwException(/plain.+object/i); - }); - - it(`should throw error if invalid maxAttempts`, function () { - const init = () => new Job(mockQueue, index, 'type1', { id: '123' }, { max_attempts: -1 }); - expect(init).to.throwException(/invalid.+max_attempts/i); - }); - }); - - describe('construction', function () { - let indexSpy; - beforeEach(function () { - type = 'type1'; - payload = { id: '123' }; - indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); - }); - - it('should create the target index', function () { - const job = new Job(mockQueue, index, type, payload, options); - return job.ready.then(() => { - sinon.assert.calledOnce(createIndexMock); - const args = createIndexMock.getCall(0).args; - expect(args[0]).to.equal(client); - expect(args[1]).to.equal(index); - }); - }); - - it('should index the payload', function () { - const job = new Job(mockQueue, index, type, payload); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs).to.have.property('index', index); - expect(indexArgs).to.have.property('body'); - expect(indexArgs.body).to.have.property('payload', payload); - }); - }); - - it('should index the job type', function () { - const job = new Job(mockQueue, index, type, payload); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs).to.have.property('index', index); - expect(indexArgs).to.have.property('body'); - expect(indexArgs.body).to.have.property('jobtype', type); - }); - }); - - it('should set event creation time', function () { - const job = new Job(mockQueue, index, type, payload); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('created_at'); - }); - }); - - it('should refresh the index', function () { - const refreshSpy = client.callAsInternalUser.withArgs('indices.refresh'); - - const job = new Job(mockQueue, index, type, payload); - return job.ready.then(() => { - sinon.assert.calledOnce(refreshSpy); - const spyCall = refreshSpy.getCall(0); - expect(spyCall.args[1]).to.have.property('index', index); - }); - }); - - it('should emit the job information on success', function (done) { - const job = new Job(mockQueue, index, type, payload); - job.once(constants.EVENT_JOB_CREATED, (jobDoc) => { - try { - expect(jobDoc).to.have.property('id'); - expect(jobDoc).to.have.property('index'); - expect(jobDoc).to.have.property('_seq_no'); - expect(jobDoc).to.have.property('_primary_term'); - done(); - } catch (e) { - done(e); - } - }); - }); - - it('should emit error on index creation failure', function (done) { - const errMsg = 'test index creation failure'; - - createIndexMock.returns(Promise.reject(new Error(errMsg))); - const job = new Job(mockQueue, index, type, payload); - - job.once(constants.EVENT_JOB_CREATE_ERROR, (err) => { - try { - expect(err.message).to.equal(errMsg); - done(); - } catch (e) { - done(e); - } - }); - }); - - it('should emit error on client index failure', function (done) { - const errMsg = 'test document index failure'; - - client.callAsInternalUser.restore(); - sinon - .stub(client, 'callAsInternalUser') - .withArgs('index') - .callsFake(() => Promise.reject(new Error(errMsg))); - const job = new Job(mockQueue, index, type, payload); - - job.once(constants.EVENT_JOB_CREATE_ERROR, (err) => { - try { - expect(err.message).to.equal(errMsg); - done(); - } catch (e) { - done(e); - } - }); - }); - }); - - describe('event emitting', function () { - it('should trigger events on the queue instance', function (done) { - const eventName = 'test event'; - const payload1 = { - test: true, - deep: { object: 'ok' }, - }; - const payload2 = 'two'; - const payload3 = new Error('test error'); - - const job = new Job(mockQueue, index, type, payload, options); - - mockQueue.on(eventName, (...args) => { - try { - expect(args[0]).to.equal(payload1); - expect(args[1]).to.equal(payload2); - expect(args[2]).to.equal(payload3); - done(); - } catch (e) { - done(e); - } - }); - - job.emit(eventName, payload1, payload2, payload3); - }); - }); - - describe('default values', function () { - let indexSpy; - beforeEach(function () { - type = 'type1'; - payload = { id: '123' }; - indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); - }); - - it('should set attempt count to 0', function () { - const job = new Job(mockQueue, index, type, payload); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('attempts', 0); - }); - }); - - it('should index default created_by value', function () { - const job = new Job(mockQueue, index, type, payload); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('created_by', defaultCreatedBy); - }); - }); - - it('should set an expired process_expiration time', function () { - const now = new Date().getTime(); - const job = new Job(mockQueue, index, type, payload); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('process_expiration'); - expect(indexArgs.body.process_expiration.getTime()).to.be.lessThan(now); - }); - }); - - it('should set status as pending', function () { - const job = new Job(mockQueue, index, type, payload); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('status', constants.JOB_STATUS_PENDING); - }); - }); - - it('should have a default priority of 10', function () { - const job = new Job(mockQueue, index, type, payload, options); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('priority', defaultPriority); - }); - }); - - it('should set a browser type', function () { - const job = new Job(mockQueue, index, type, payload); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('browser_type'); - }); - }); - }); - - describe('option passing', function () { - let indexSpy; - beforeEach(function () { - type = 'type1'; - payload = { id: '123' }; - options = { - timeout: 4567, - max_attempts: 9, - headers: { - authorization: 'Basic cXdlcnR5', - }, - }; - indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); - }); - - it('should index the created_by value', function () { - const createdBy = 'user_identifier'; - const job = new Job(mockQueue, index, type, payload, { - created_by: createdBy, - ...options, - }); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('created_by', createdBy); - }); - }); - - it('should index timeout value from options', function () { - const job = new Job(mockQueue, index, type, payload, options); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('timeout', options.timeout); - }); - }); - - it('should set max attempt count', function () { - const job = new Job(mockQueue, index, type, payload, options); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('max_attempts', options.max_attempts); - }); - }); - - it('should add headers to the request params', function () { - const job = new Job(mockQueue, index, type, payload, options); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs).to.have.property('headers', options.headers); - }); - }); - - it(`should use upper priority of ${maxPriority}`, function () { - const job = new Job(mockQueue, index, type, payload, { priority: maxPriority * 2 }); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('priority', maxPriority); - }); - }); - - it(`should use lower priority of ${minPriority}`, function () { - const job = new Job(mockQueue, index, type, payload, { priority: minPriority * 2 }); - return job.ready.then(() => { - const indexArgs = validateDoc(indexSpy); - expect(indexArgs.body).to.have.property('priority', minPriority); - }); - }); - }); - - describe('get method', function () { - beforeEach(function () { - type = 'type2'; - payload = { id: '123' }; - }); - - it('should return the job document', function () { - const job = new Job(mockQueue, index, type, payload); - - return job.get().then((doc) => { - const jobDoc = job.document; // document should be resolved - expect(doc).to.have.property('index', index); - expect(doc).to.have.property('id', jobDoc.id); - expect(doc).to.have.property('_seq_no', jobDoc._seq_no); - expect(doc).to.have.property('_primary_term', jobDoc._primary_term); - expect(doc).to.have.property('created_by', defaultCreatedBy); - - expect(doc).to.have.property('payload'); - expect(doc).to.have.property('jobtype'); - expect(doc).to.have.property('priority'); - expect(doc).to.have.property('timeout'); - }); - }); - - it('should contain optional data', function () { - const optionals = { - created_by: 'some_ident', - }; - - const job = new Job(mockQueue, index, type, payload, optionals); - return Promise.resolve(client.callAsInternalUser('get', {}, optionals)) - .then((doc) => { - sinon.stub(client, 'callAsInternalUser').withArgs('get').returns(Promise.resolve(doc)); - }) - .then(() => { - return job.get().then((doc) => { - expect(doc).to.have.property('created_by', optionals.created_by); - }); - }); - }); - }); - - describe('toJSON method', function () { - beforeEach(function () { - type = 'type2'; - payload = { id: '123' }; - options = { - timeout: 4567, - max_attempts: 9, - priority: 8, - }; - }); - - it('should return the static information about the job', function () { - const job = new Job(mockQueue, index, type, payload, options); - - // toJSON is sync, should work before doc is written to elasticsearch - expect(job.document).to.be(undefined); - - const doc = job.toJSON(); - expect(doc).to.have.property('index', index); - expect(doc).to.have.property('jobtype', type); - expect(doc).to.have.property('created_by', defaultCreatedBy); - expect(doc).to.have.property('timeout', options.timeout); - expect(doc).to.have.property('max_attempts', options.max_attempts); - expect(doc).to.have.property('priority', options.priority); - expect(doc).to.have.property('id'); - expect(doc).to.not.have.property('version'); - }); - - it('should contain optional data', function () { - const optionals = { - created_by: 'some_ident', - }; - - const job = new Job(mockQueue, index, type, payload, optionals); - const doc = job.toJSON(); - expect(doc).to.have.property('created_by', optionals.created_by); - }); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/esqueue/index.js b/x-pack/plugins/reporting/server/lib/esqueue/index.js index 735d19f8f6c47..0fbcb54c673dd 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/index.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/index.js @@ -5,20 +5,17 @@ */ import { EventEmitter } from 'events'; -import { Job } from './job'; import { Worker } from './worker'; import { constants } from './constants'; -import { indexTimestamp } from './helpers/index_timestamp'; import { omit } from 'lodash'; export { events } from './constants/events'; export class Esqueue extends EventEmitter { - constructor(index, options = {}) { - if (!index) throw new Error('Must specify an index to write to'); - + constructor(store, options = {}) { super(); - this.index = index; + this.store = store; // for updating jobs in ES + this.index = this.store.indexPrefix; // for polling for pending jobs this.settings = { interval: constants.DEFAULT_SETTING_INTERVAL, timeout: constants.DEFAULT_SETTING_TIMEOUT, @@ -40,21 +37,6 @@ export class Esqueue extends EventEmitter { }); } - addJob(jobtype, payload, opts = {}) { - const timestamp = indexTimestamp(this.settings.interval, this.settings.dateSeparator); - const index = `${this.index}-${timestamp}`; - const defaults = { - timeout: this.settings.timeout, - }; - - const options = Object.assign(defaults, opts, { - indexSettings: this.settings.indexSettings, - logger: this._logger, - }); - - return new Job(this, index, jobtype, payload, options); - } - registerWorker(type, workerFn, opts) { const worker = new Worker(this, type, workerFn, { ...opts, logger: this._logger }); this._workers.push(worker); diff --git a/x-pack/plugins/reporting/server/lib/esqueue/job.js b/x-pack/plugins/reporting/server/lib/esqueue/job.js deleted file mode 100644 index 6ab78eeb1b86b..0000000000000 --- a/x-pack/plugins/reporting/server/lib/esqueue/job.js +++ /dev/null @@ -1,142 +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 events from 'events'; -import Puid from 'puid'; -import { constants } from './constants'; -import { createIndex } from './helpers/create_index'; -import { isPlainObject } from 'lodash'; - -const puid = new Puid(); - -export class Job extends events.EventEmitter { - constructor(queue, index, jobtype, payload, options = {}) { - if (typeof jobtype !== 'string') throw new Error('Jobtype must be a string'); - if (!isPlainObject(payload)) throw new Error('Payload must be a plain object'); - - super(); - - this.queue = queue; - this._client = this.queue.client; - this.id = puid.generate(); - this.index = index; - this.jobtype = jobtype; - this.payload = payload; - this.created_by = options.created_by || false; - this.timeout = options.timeout || 10000; - this.maxAttempts = options.max_attempts || 3; - this.priority = Math.max(Math.min(options.priority || 10, 20), -20); - this.indexSettings = options.indexSettings || {}; - this.browser_type = options.browser_type; - - if (typeof this.maxAttempts !== 'number' || this.maxAttempts < 1) { - throw new Error(`Invalid max_attempts: ${this.maxAttempts}`); - } - - this.debug = (msg, err) => { - const logger = options.logger || function () {}; - const message = `${this.id} - ${msg}`; - const tags = ['debug']; - - if (err) { - logger(`${message}: ${err}`, tags); - return; - } - - logger(message, tags); - }; - - const indexParams = { - index: this.index, - id: this.id, - body: { - jobtype: this.jobtype, - meta: { - // We are copying these values out of payload because these fields are indexed and can be aggregated on - // for tracking stats, while payload contents are not. - objectType: payload.objectType, - layout: payload.layout ? payload.layout.id : 'none', - }, - payload: this.payload, - priority: this.priority, - created_by: this.created_by, - timeout: this.timeout, - process_expiration: new Date(0), // use epoch so the job query works - created_at: new Date(), - attempts: 0, - max_attempts: this.maxAttempts, - status: constants.JOB_STATUS_PENDING, - browser_type: this.browser_type, - }, - }; - - if (options.headers) { - indexParams.headers = options.headers; - } - - this.ready = createIndex(this._client, this.index, this.indexSettings) - .then(() => this._client.callAsInternalUser('index', indexParams)) - .then((doc) => { - this.document = { - id: doc._id, - index: doc._index, - _seq_no: doc._seq_no, - _primary_term: doc._primary_term, - }; - this.debug(`Job created in index ${this.index}`); - - return this._client - .callAsInternalUser('indices.refresh', { - index: this.index, - }) - .then(() => { - this.debug(`Job index refreshed ${this.index}`); - this.emit(constants.EVENT_JOB_CREATED, this.document); - }); - }) - .catch((err) => { - this.debug('Job creation failed', err); - this.emit(constants.EVENT_JOB_CREATE_ERROR, err); - }); - } - - emit(name, ...args) { - super.emit(name, ...args); - this.queue.emit(name, ...args); - } - - get() { - return this.ready - .then(() => { - return this._client.callAsInternalUser('get', { - index: this.index, - id: this.id, - }); - }) - .then((doc) => { - return Object.assign(doc._source, { - index: doc._index, - id: doc._id, - _seq_no: doc._seq_no, - _primary_term: doc._primary_term, - }); - }); - } - - toJSON() { - return { - id: this.id, - index: this.index, - jobtype: this.jobtype, - created_by: this.created_by, - payload: this.payload, - timeout: this.timeout, - max_attempts: this.maxAttempts, - priority: this.priority, - browser_type: this.browser_type, - }; - } -} diff --git a/x-pack/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/plugins/reporting/server/lib/esqueue/worker.js index b26ed731c6831..469bafd694612 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/plugins/reporting/server/lib/esqueue/worker.js @@ -158,8 +158,8 @@ export class Worker extends events.EventEmitter { kibana_name: this.kibanaName, }; - return this._client - .callAsInternalUser('update', { + return this.queue.store + .updateReport({ index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -197,8 +197,8 @@ export class Worker extends events.EventEmitter { output: docOutput, }); - return this._client - .callAsInternalUser('update', { + return this.queue.store + .updateReport({ index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -294,8 +294,8 @@ export class Worker extends events.EventEmitter { output: docOutput, }; - return this._client - .callAsInternalUser('update', { + return this.queue.store + .updateReport({ index: job._index, id: job._id, if_seq_no: job._seq_no, diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index 0e9c49b170887..f5a50fca28b7a 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -12,3 +12,4 @@ export { enqueueJobFactory } from './enqueue_job'; export { getExportTypesRegistry } from './export_types_registry'; export { runValidations } from './validate'; export { startTrace } from './trace'; +export { ReportingStore } from './store'; diff --git a/x-pack/plugins/maps/public/classes/sources/vector_feature_types.ts b/x-pack/plugins/reporting/server/lib/store/index.ts similarity index 72% rename from x-pack/plugins/maps/public/classes/sources/vector_feature_types.ts rename to x-pack/plugins/reporting/server/lib/store/index.ts index 9f03357e17dad..a88d36d3fdf9a 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_feature_types.ts +++ b/x-pack/plugins/reporting/server/lib/store/index.ts @@ -4,8 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum VECTOR_SHAPE_TYPES { - POINT = 'POINT', - LINE = 'LINE', - POLYGON = 'POLYGON', -} +export { Report } from './report'; +export { ReportingStore } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts similarity index 80% rename from x-pack/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js rename to x-pack/plugins/reporting/server/lib/store/index_timestamp.ts index ceb4ef43b2d9d..71ce0b1e572f8 100644 --- a/x-pack/plugins/reporting/server/lib/esqueue/helpers/index_timestamp.js +++ b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts @@ -4,19 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; +import moment, { unitOfTime } from 'moment'; export const intervals = ['year', 'month', 'week', 'day', 'hour', 'minute']; // TODO: This helper function can be removed by using `schema.duration` objects in the reporting config schema -export function indexTimestamp(intervalStr, separator = '-') { +export function indexTimestamp(intervalStr: string, separator = '-') { + const startOf = intervalStr as unitOfTime.StartOf; if (separator.match(/[a-z]/i)) throw new Error('Interval separator can not be a letter'); const index = intervals.indexOf(intervalStr); - if (index === -1) throw new Error('Invalid index interval: ', intervalStr); + if (index === -1) throw new Error('Invalid index interval: ' + intervalStr); const m = moment(); - m.startOf(intervalStr); + m.startOf(startOf); let dateString; switch (intervalStr) { diff --git a/x-pack/plugins/reporting/server/lib/store/mapping.ts b/x-pack/plugins/reporting/server/lib/store/mapping.ts new file mode 100644 index 0000000000000..a819923e2f105 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/mapping.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mapping = { + meta: { + // We are indexing these properties with both text and keyword fields because that's what will be auto generated + // when an index already exists. This schema is only used when a reporting index doesn't exist. This way existing + // reporting indexes and new reporting indexes will look the same and the data can be queried in the same + // manner. + properties: { + /** + * Type of object that is triggering this report. Should be either search, visualization or dashboard. + * Used for job listing and telemetry stats only. + */ + objectType: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + /** + * Can be either preserve_layout, print or none (in the case of csv export). + * Used for phone home stats only. + */ + layout: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, + browser_type: { type: 'keyword' }, + jobtype: { type: 'keyword' }, + payload: { type: 'object', enabled: false }, + priority: { type: 'byte' }, + timeout: { type: 'long' }, + process_expiration: { type: 'date' }, + created_by: { type: 'keyword' }, + created_at: { type: 'date' }, + started_at: { type: 'date' }, + completed_at: { type: 'date' }, + attempts: { type: 'short' }, + max_attempts: { type: 'short' }, + kibana_name: { type: 'keyword' }, + kibana_id: { type: 'keyword' }, + status: { type: 'keyword' }, + output: { + type: 'object', + properties: { + content_type: { type: 'keyword' }, + size: { type: 'long' }, + content: { type: 'object', enabled: false }, + }, + }, +}; diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts new file mode 100644 index 0000000000000..83444494e61d3 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Report } from './report'; + +describe('Class Report', () => { + it('constructs Report instance', () => { + const opts = { + index: '.reporting-test-index-12345', + jobtype: 'test-report', + created_by: 'created_by_test_string', + browser_type: 'browser_type_test_string', + max_attempts: 50, + payload: { payload_test_field: 1 }, + timeout: 30000, + priority: 1, + }; + const report = new Report(opts); + expect(report.toJSON()).toMatchObject({ + _primary_term: undefined, + _seq_no: undefined, + browser_type: 'browser_type_test_string', + created_by: 'created_by_test_string', + jobtype: 'test-report', + max_attempts: 50, + payload: { + payload_test_field: 1, + }, + priority: 1, + timeout: 30000, + }); + + expect(report.id).toBeDefined(); + }); + + it('updateWithDoc method syncs takes fields to sync ES metadata', () => { + const opts = { + index: '.reporting-test-index-12345', + jobtype: 'test-report', + created_by: 'created_by_test_string', + browser_type: 'browser_type_test_string', + max_attempts: 50, + payload: { payload_test_field: 1 }, + timeout: 30000, + priority: 1, + }; + const report = new Report(opts); + + const metadata = { + _index: '.reporting-test-update', + _id: '12342p9o387549o2345', + _primary_term: 77, + _seq_no: 99, + }; + report.updateWithDoc(metadata); + + expect(report.toJSON()).toMatchObject({ + index: '.reporting-test-update', + _primary_term: 77, + _seq_no: 99, + browser_type: 'browser_type_test_string', + created_by: 'created_by_test_string', + jobtype: 'test-report', + max_attempts: 50, + payload: { + payload_test_field: 1, + }, + priority: 1, + timeout: 30000, + }); + + expect(report._id).toBe('12342p9o387549o2345'); + }); +}); diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts new file mode 100644 index 0000000000000..cc9967e64b6eb --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore no module definition +import Puid from 'puid'; + +interface Payload { + id?: string; + index: string; + jobtype: string; + created_by: string | boolean; + payload: unknown; + browser_type: string; + priority: number; + max_attempts: number; + timeout: number; +} + +const puid = new Puid(); + +export class Report { + public readonly jobtype: string; + public readonly created_by: string | boolean; + public readonly payload: unknown; + public readonly browser_type: string; + public readonly id: string; + + public readonly priority: number; + // queue stuff, to be removed with Task Manager integration + public readonly max_attempts: number; + public readonly timeout: number; + + public _index: string; + public _id?: string; // set by ES + public _primary_term?: unknown; // set by ES + public _seq_no: unknown; // set by ES + + /* + * Create an unsaved report + */ + constructor(opts: Payload) { + this.jobtype = opts.jobtype; + this.created_by = opts.created_by; + this.payload = opts.payload; + this.browser_type = opts.browser_type; + this.priority = opts.priority; + this.max_attempts = opts.max_attempts; + this.timeout = opts.timeout; + this.id = puid.generate(); + + this._index = opts.index; + } + + /* + * Update the report with "live" storage metadata + */ + updateWithDoc(doc: Partial) { + if (doc._index) { + this._index = doc._index; // can not be undefined + } + + this._id = doc._id; + this._primary_term = doc._primary_term; + this._seq_no = doc._seq_no; + } + + toJSON() { + return { + id: this.id, + index: this._index, + _seq_no: this._seq_no, + _primary_term: this._primary_term, + jobtype: this.jobtype, + created_by: this.created_by, + payload: this.payload, + timeout: this.timeout, + max_attempts: this.max_attempts, + priority: this.priority, + browser_type: this.browser_type, + }; + } +} diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts new file mode 100644 index 0000000000000..4868a1dfdd8f3 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { ReportingConfig, ReportingCore } from '../..'; +import { createMockReportingCore } from '../../test_helpers'; +import { createMockLevelLogger } from '../../test_helpers/create_mock_levellogger'; +import { ReportingStore } from './store'; +import { ElasticsearchServiceSetup } from 'src/core/server'; + +const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ + get: mockConfigGet, + kbnConfig: { get: mockConfigGet }, +}); + +describe('ReportingStore', () => { + const mockLogger = createMockLevelLogger(); + let mockConfig: ReportingConfig; + let mockCore: ReportingCore; + + const callClusterStub = sinon.stub(); + const mockElasticsearch = { legacy: { client: { callAsInternalUser: callClusterStub } } }; + + beforeEach(async () => { + const mockConfigGet = sinon.stub(); + mockConfigGet.withArgs('index').returns('.reporting-test'); + mockConfigGet.withArgs('queue', 'indexInterval').returns('week'); + mockConfig = getMockConfig(mockConfigGet); + mockCore = await createMockReportingCore(mockConfig); + + callClusterStub.withArgs('indices.exists').resolves({}); + callClusterStub.withArgs('indices.create').resolves({}); + callClusterStub.withArgs('index').resolves({}); + callClusterStub.withArgs('indices.refresh').resolves({}); + callClusterStub.withArgs('update').resolves({}); + + mockCore.getElasticsearchService = () => + (mockElasticsearch as unknown) as ElasticsearchServiceSetup; + }); + + describe('addReport', () => { + it('returns Report object', async () => { + const store = new ReportingStore(mockCore, mockLogger); + const reportType = 'unknowntype'; + const reportPayload = {}; + const reportOptions = { + timeout: 10000, + created_by: 'created_by_string', + browser_type: 'browser_type_string', + max_attempts: 1, + }; + await expect( + store.addReport(reportType, reportPayload, reportOptions) + ).resolves.toMatchObject({ + _primary_term: undefined, + _seq_no: undefined, + browser_type: 'browser_type_string', + created_by: 'created_by_string', + jobtype: 'unknowntype', + max_attempts: 1, + payload: {}, + priority: 10, + timeout: 10000, + }); + }); + + it('throws if options has invalid indexInterval', async () => { + const mockConfigGet = sinon.stub(); + mockConfigGet.withArgs('index').returns('.reporting-test'); + mockConfigGet.withArgs('queue', 'indexInterval').returns('centurially'); + mockConfig = getMockConfig(mockConfigGet); + mockCore = await createMockReportingCore(mockConfig); + + const store = new ReportingStore(mockCore, mockLogger); + const reportType = 'unknowntype'; + const reportPayload = {}; + const reportOptions = { + timeout: 10000, + created_by: 'created_by_string', + browser_type: 'browser_type_string', + max_attempts: 1, + }; + expect( + store.addReport(reportType, reportPayload, reportOptions) + ).rejects.toMatchInlineSnapshot(`[Error: Invalid index interval: centurially]`); + }); + + it('handles error creating the index', async () => { + // setup + callClusterStub.withArgs('indices.exists').resolves(false); + callClusterStub.withArgs('indices.create').rejects(new Error('error')); + + const store = new ReportingStore(mockCore, mockLogger); + const reportType = 'unknowntype'; + const reportPayload = {}; + const reportOptions = { + timeout: 10000, + created_by: 'created_by_string', + browser_type: 'browser_type_string', + max_attempts: 1, + }; + await expect( + store.addReport(reportType, reportPayload, reportOptions) + ).rejects.toMatchInlineSnapshot(`[Error: error]`); + }); + + /* Creating the index will fail, if there were multiple jobs staged in + * parallel and creation completed from another Kibana instance. Only the + * first request in line can successfully create it. + * In spite of that race condition, adding the new job in Elasticsearch is + * fine. + */ + it('ignores index creation error if the index already exists and continues adding the report', async () => { + // setup + callClusterStub.withArgs('indices.exists').resolves(false); + callClusterStub.withArgs('indices.create').rejects(new Error('error')); + + const store = new ReportingStore(mockCore, mockLogger); + const reportType = 'unknowntype'; + const reportPayload = {}; + const reportOptions = { + timeout: 10000, + created_by: 'created_by_string', + browser_type: 'browser_type_string', + max_attempts: 1, + }; + await expect( + store.addReport(reportType, reportPayload, reportOptions) + ).rejects.toMatchInlineSnapshot(`[Error: error]`); + }); + + it('skips creating the index if already exists', async () => { + // setup + callClusterStub.withArgs('indices.exists').resolves(false); + callClusterStub + .withArgs('indices.create') + .rejects(new Error('resource_already_exists_exception')); // will be triggered but ignored + + const store = new ReportingStore(mockCore, mockLogger); + const reportType = 'unknowntype'; + const reportPayload = {}; + const reportOptions = { + timeout: 10000, + created_by: 'created_by_string', + browser_type: 'browser_type_string', + max_attempts: 1, + }; + await expect( + store.addReport(reportType, reportPayload, reportOptions) + ).resolves.toMatchObject({ + _primary_term: undefined, + _seq_no: undefined, + browser_type: 'browser_type_string', + created_by: 'created_by_string', + jobtype: 'unknowntype', + max_attempts: 1, + payload: {}, + priority: 10, + timeout: 10000, + }); + }); + }); +}); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts new file mode 100644 index 0000000000000..1cb964a7bbfac --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ElasticsearchServiceSetup } from 'src/core/server'; +import { LevelLogger } from '../'; +import { ReportingCore } from '../../'; +import { LayoutInstance } from '../../export_types/common/layouts'; +import { indexTimestamp } from './index_timestamp'; +import { mapping } from './mapping'; +import { Report } from './report'; + +export const statuses = { + JOB_STATUS_PENDING: 'pending', + JOB_STATUS_PROCESSING: 'processing', + JOB_STATUS_COMPLETED: 'completed', + JOB_STATUS_WARNINGS: 'completed_with_warnings', + JOB_STATUS_FAILED: 'failed', + JOB_STATUS_CANCELLED: 'cancelled', +}; + +interface AddReportOpts { + timeout: number; + created_by: string | boolean; + browser_type: string; + max_attempts: number; +} + +interface UpdateQuery { + index: string; + id: string; + if_seq_no: unknown; + if_primary_term: unknown; + body: { doc: Partial }; +} + +/* + * A class to give an interface to historical reports in the reporting.index + * - track the state: pending, processing, completed, etc + * - handle updates and deletes to the reporting document + * - interface for downloading the report + */ +export class ReportingStore { + public readonly indexPrefix: string; + public readonly indexInterval: string; + + private client: ElasticsearchServiceSetup['legacy']['client']; + private logger: LevelLogger; + + constructor(reporting: ReportingCore, logger: LevelLogger) { + const config = reporting.getConfig(); + const elasticsearch = reporting.getElasticsearchService(); + + this.client = elasticsearch.legacy.client; + this.indexPrefix = config.get('index'); + this.indexInterval = config.get('queue', 'indexInterval'); + + this.logger = logger; + } + + private async createIndex(indexName: string) { + return this.client + .callAsInternalUser('indices.exists', { + index: indexName, + }) + .then((exists) => { + if (exists) { + return exists; + } + + const indexSettings = { + number_of_shards: 1, + auto_expand_replicas: '0-1', + }; + const body = { + settings: indexSettings, + mappings: { + properties: mapping, + }, + }; + + return this.client + .callAsInternalUser('indices.create', { + index: indexName, + body, + }) + .then(() => true) + .catch((err: Error) => { + const isIndexExistsError = err.message.match(/resource_already_exists_exception/); + if (isIndexExistsError) { + // Do not fail a job if the job runner hits the race condition. + this.logger.warn(`Automatic index creation failed: index already exists: ${err}`); + return; + } + + throw err; + }); + }); + } + + private async saveReport(report: Report) { + const payload = report.payload as { objectType: string; layout: LayoutInstance }; + + const indexParams = { + index: report._index, + id: report.id, + body: { + jobtype: report.jobtype, + meta: { + // We are copying these values out of payload because these fields are indexed and can be aggregated on + // for tracking stats, while payload contents are not. + objectType: payload.objectType, + layout: payload.layout ? payload.layout.id : 'none', + }, + payload: report.payload, + created_by: report.created_by, + timeout: report.timeout, + process_expiration: new Date(0), // use epoch so the job query works + created_at: new Date(), + attempts: 0, + max_attempts: report.max_attempts, + status: statuses.JOB_STATUS_PENDING, + browser_type: report.browser_type, + }, + }; + return this.client.callAsInternalUser('index', indexParams); + } + + private async refreshIndex(index: string) { + return this.client.callAsInternalUser('indices.refresh', { index }); + } + + public async addReport(type: string, payload: unknown, options: AddReportOpts): Promise { + const timestamp = indexTimestamp(this.indexInterval); + const index = `${this.indexPrefix}-${timestamp}`; + await this.createIndex(index); + + const report = new Report({ + index, + payload, + jobtype: type, + created_by: options.created_by, + browser_type: options.browser_type, + max_attempts: options.max_attempts, + timeout: options.timeout, + priority: 10, // unused + }); + + const doc = await this.saveReport(report); + report.updateWithDoc(doc); + + await this.refreshIndex(index); + this.logger.info(`Successfully queued pending job: ${report._index}/${report.id}`); + + return report; + } + + public async updateReport(query: UpdateQuery): Promise { + return this.client.callAsInternalUser('update', { + index: query.index, + id: query.id, + if_seq_no: query.if_seq_no, + if_primary_term: query.if_primary_term, + body: { doc: query.body.doc }, + }); + } +} diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 693b0917603fc..cedc9dc14a237 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -8,7 +8,13 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core import { ReportingCore } from './'; import { initializeBrowserDriverFactory } from './browsers'; import { buildConfig, ReportingConfigType } from './config'; -import { createQueueFactory, enqueueJobFactory, LevelLogger, runValidations } from './lib'; +import { + createQueueFactory, + enqueueJobFactory, + LevelLogger, + runValidations, + ReportingStore, +} from './lib'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; @@ -86,9 +92,9 @@ export class ReportingPlugin const config = reportingCore.getConfig(); const browserDriverFactory = await initializeBrowserDriverFactory(config, logger); - - const esqueue = await createQueueFactory(reportingCore, logger); // starts polling for pending jobs - const enqueueJob = enqueueJobFactory(reportingCore, logger); // called from generation routes + const store = new ReportingStore(reportingCore, logger); + const esqueue = await createQueueFactory(reportingCore, store, logger); // starts polling for pending jobs + const enqueueJob = enqueueJobFactory(reportingCore, store, logger); // called from generation routes reportingCore.pluginStart({ browserDriverFactory, @@ -96,6 +102,7 @@ export class ReportingPlugin uiSettings: core.uiSettings, esqueue, enqueueJob, + store, }); // run self-check validations diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts new file mode 100644 index 0000000000000..f5e9a44281cb6 --- /dev/null +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LevelLogger } from '../lib'; + +export function createMockLevelLogger() { + // eslint-disable-next-line no-console + const consoleLogger = (tag: string) => (message: unknown) => console.log(tag, message); + const innerLogger = { + get: () => innerLogger, + debug: consoleLogger('debug'), + info: consoleLogger('info'), + warn: consoleLogger('warn'), + trace: consoleLogger('trace'), + error: consoleLogger('error'), + fatal: consoleLogger('fatal'), + log: consoleLogger('log'), + }; + return new LevelLogger(innerLogger); +} diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 579035a46f615..427a6362a7258 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -20,6 +20,8 @@ import { } from '../browsers'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStartDeps } from '../types'; +import { ReportingStore } from '../lib'; +import { createMockLevelLogger } from './create_mock_levellogger'; (initializeBrowserDriverFactory as jest.Mock< Promise @@ -37,13 +39,19 @@ const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup => { }; }; -const createMockPluginStart = (startMock?: any): ReportingInternalStart => { +const createMockPluginStart = ( + mockReportingCore: ReportingCore, + startMock?: any +): ReportingInternalStart => { + const logger = createMockLevelLogger(); + const store = new ReportingStore(mockReportingCore, logger); return { browserDriverFactory: startMock.browserDriverFactory, enqueueJob: startMock.enqueueJob, esqueue: startMock.esqueue, savedObjects: startMock.savedObjects || { getScopedClient: jest.fn() }, uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) }, + store, }; }; @@ -60,9 +68,22 @@ export const createMockStartDeps = (startMock?: any): ReportingStartDeps => ({ export const createMockReportingCore = async ( config: ReportingConfig, - setupDepsMock: ReportingInternalSetup | undefined = createMockPluginSetup({}), - startDepsMock: ReportingInternalStart | undefined = createMockPluginStart({}) + setupDepsMock: ReportingInternalSetup | undefined = undefined, + startDepsMock: ReportingInternalStart | undefined = undefined ) => { + if (!setupDepsMock) { + setupDepsMock = createMockPluginSetup({}); + } + + const mockReportingCore = { + getConfig: () => config, + getElasticsearchService: () => setupDepsMock?.elasticsearch, + } as ReportingCore; + + if (!startDepsMock) { + startDepsMock = createMockPluginStart(mockReportingCore, {}); + } + config = config || {}; const core = new ReportingCore(); diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 4f13fd97ce442..42f5f4b220da9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -74,7 +74,7 @@ export interface ResolverNodeStats { /** * A child node can also have additional children so we need to provide a pagination cursor. */ -export interface ChildNode extends LifecycleNode { +export interface ResolverChildNode extends ResolverLifecycleNode { /** * A child node's pagination cursor can be null for a couple reasons: * 1. At the time of querying it could have no children in ES, in which case it will be marked as @@ -89,7 +89,7 @@ export interface ChildNode extends LifecycleNode { * has an array of lifecycle events. */ export interface ResolverChildren { - childNodes: ChildNode[]; + childNodes: ResolverChildNode[]; /** * This is the children cursor for the origin of a tree. */ @@ -116,7 +116,7 @@ export interface ResolverTree { /** * The lifecycle events (start, end etc) for a node. */ -export interface LifecycleNode { +export interface ResolverLifecycleNode { entityID: string; lifecycle: ResolverEvent[]; /** @@ -132,7 +132,7 @@ export interface ResolverAncestry { /** * An array of ancestors with the lifecycle events grouped together */ - ancestors: LifecycleNode[]; + ancestors: ResolverLifecycleNode[]; /** * A cursor for retrieving additional ancestors for a particular node. `null` indicates that there were no additional * ancestors when the request returned. More could have been ingested by ES after the fact though. diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 16207fcec3b26..ee1dc73b27fe2 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -20,6 +20,7 @@ import { ManageGlobalTimeline, timelineDefaults, } from '../../../timelines/components/manage_timeline'; +import { TimelineId } from '../../../../common/types/timeline'; jest.mock('../link_to'); @@ -41,9 +42,24 @@ jest.mock('uuid', () => { }); jest.mock('../../hooks/use_add_to_timeline'); +const mockAddFilters = jest.fn(); +const mockGetTimelineFilterManager = jest.fn().mockReturnValue({ + addFilters: mockAddFilters, +}); +jest.mock('../../../timelines/components/manage_timeline', () => { + const original = jest.requireActual('../../../timelines/components/manage_timeline'); + + return { + ...original, + useManageTimeline: () => ({ + getTimelineFilterManager: mockGetTimelineFilterManager, + isManagedTimeline: jest.fn().mockReturnValue(false), + }), + }; +}); const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; -const timelineId = 'cool-id'; +const timelineId = TimelineId.active; const field = 'process.name'; const value = 'nice'; const toggleTopN = jest.fn(); @@ -88,6 +104,9 @@ describe('DraggableWrapperHoverContent', () => { forOrOut.forEach((hoverAction) => { describe(`Filter ${hoverAction} value`, () => { + beforeEach(() => { + jest.clearAllMocks(); + }); test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => { const wrapper = mount( @@ -111,21 +130,16 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists() ).toBe(false); }); - describe('when run in the context of a timeline', () => { - let filterManager: FilterManager; let wrapper: ReactWrapper; let onFilterAdded: () => void; beforeEach(() => { - filterManager = new FilterManager(mockUiSettingsForFilterManager); - filterManager.addFilters = jest.fn(); onFilterAdded = jest.fn(); const manageTimelineForTesting = { [timelineId]: { ...timelineDefaults, id: timelineId, - filterManager, }, }; @@ -141,7 +155,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); - expect(filterManager.addFilters).toBeCalledWith({ + expect(mockAddFilters).toBeCalledWith({ meta: { alias: null, disabled: false, @@ -174,7 +188,9 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -263,7 +279,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); - expect(filterManager.addFilters).toBeCalledWith(expected); + expect(mockAddFilters).toBeCalledWith(expected); }); }); @@ -278,7 +294,14 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -544,4 +567,41 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false); }); }); + + describe('Filter Manager', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('filter manager, not active timeline', () => { + mount( + + + + ); + + expect(mockGetTimelineFilterManager).not.toBeCalled(); + }); + test('filter manager, active timeline', () => { + mount( + + + + ); + + expect(mockGetTimelineFilterManager).toBeCalled(); + }); + test('filter manager, active timeline in draggableId', () => { + mount( + + + + ); + + expect(mockGetTimelineFilterManager).toBeCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index e805750cf2477..4efdea5eee43b 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -13,11 +13,12 @@ import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; import { createFilter } from '../add_filter_to_global_search_bar'; -import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '../top_n'; +import { StatefulTopN } from '../top_n'; import { allowTopN } from './helpers'; import * as i18n from './translations'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { TimelineId } from '../../../../common/types/timeline'; interface Props { draggableId?: DraggableId; @@ -34,7 +35,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ field, onFilterAdded, showTopN, - timelineId = ACTIVE_TIMELINE_REDUX_ID, + timelineId, toggleTopN, value, }) => { @@ -44,11 +45,16 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ kibana.services.data.query.filterManager, ]); const { getTimelineFilterManager } = useManageTimeline(); - const filterManager = useMemo(() => getTimelineFilterManager(timelineId) ?? filterManagerBackup, [ - timelineId, - getTimelineFilterManager, - filterManagerBackup, - ]); + + const filterManager = useMemo( + () => + timelineId === TimelineId.active || + (draggableId != null && draggableId?.includes(TimelineId.active)) + ? getTimelineFilterManager(TimelineId.active) + : filterManagerBackup, + [draggableId, timelineId, getTimelineFilterManager, filterManagerBackup] + ); + const filterForValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index c30c3668638a3..69e4ac615ebf2 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -17,6 +17,24 @@ jest.mock('../../utils/apollo_context', () => ({ })); describe('Index Fields & Browser Fields', () => { + test('At initialization the value of indicesExists should be true', async () => { + const { result, waitForNextUpdate } = renderHook(() => useWithSource()); + const initialResult = result.current; + + await waitForNextUpdate(); + + return expect(initialResult).toEqual({ + browserFields: {}, + errorMessage: null, + indexPattern: { + fields: [], + title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + }, + indicesExist: true, + loading: true, + }); + }); + test('returns memoized value', async () => { const { result, waitForNextUpdate, rerender } = renderHook(() => useWithSource()); await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 34ac5f8f5d94f..5e80953914c97 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -102,7 +102,7 @@ export const useWithSource = (sourceId = 'default', indexToAdd?: string[] | null browserFields: EMPTY_BROWSER_FIELDS, errorMessage: null, indexPattern: getIndexFields(defaultIndex.join(), []), - indicesExist: undefined, + indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined), loading: false, }); diff --git a/x-pack/plugins/security_solution/public/common/translations.ts b/x-pack/plugins/security_solution/public/common/translations.ts index b5a400d187f82..677543ec0dba6 100644 --- a/x-pack/plugins/security_solution/public/common/translations.ts +++ b/x-pack/plugins/security_solution/public/common/translations.ts @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; export const EMPTY_TITLE = i18n.translate('xpack.securitySolution.pages.common.emptyTitle', { - defaultMessage: 'Welcome to SIEM. Let’s get you started.', + defaultMessage: 'Welcome to Security Solution. Let’s get you started.', }); export const EMPTY_MESSAGE = i18n.translate('xpack.securitySolution.pages.common.emptyMessage', { defaultMessage: - 'To begin using security information and event management (SIEM), you’ll need to add SIEM-related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', + 'To begin using security information and event management (Security Solution), you’ll need to add security solution related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', }); export const EMPTY_ACTION_PRIMARY = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 360c81abadc81..b247170a4a5db 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -16,6 +16,7 @@ import { PluginInitializerContext, Plugin as IPlugin, DEFAULT_APP_CATEGORIES, + AppNavLinkStatus, } from '../../../../src/core/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; @@ -35,6 +36,7 @@ import { APP_CASES_PATH, SHOW_ENDPOINT_ALERTS_NAV, APP_ENDPOINT_ALERTS_PATH, + APP_PATH, } from '../common/constants'; import { ConfigureEndpointDatasource } from './management/pages/policy/view/ingest_manager_integration/configure_datasource'; @@ -86,18 +88,18 @@ export class Plugin implements IPlugin { - // const [{ application }] = await core.getStartServices(); - // application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true }); - // return () => true; - // }, - // }); + core.application.register({ + exactRoute: true, + id: APP_ID, + title: 'Security', + appRoute: APP_PATH, + navLinkStatus: AppNavLinkStatus.hidden, + mount: async (params: AppMountParameters) => { + const [{ application }] = await core.getStartServices(); + application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true }); + return () => true; + }, + }); core.application.register({ id: `${APP_ID}:${SecurityPageName.overview}`, diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts index a352a076e5a97..343b4e1a14478 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts @@ -12,7 +12,7 @@ import { ResolverEvent, ResolverChildren, ResolverAncestry, - LifecycleNode, + ResolverLifecycleNode, ResolverNodeStats, ResolverRelatedEvents, } from '../../../common/endpoint/types'; @@ -25,10 +25,10 @@ type MiddlewareFactory = ( ) => (next: Dispatch) => (action: ResolverAction) => unknown; function getLifecycleEventsAndStats( - nodes: LifecycleNode[], + nodes: ResolverLifecycleNode[], stats: Map ): ResolverEvent[] { - return nodes.reduce((flattenedEvents: ResolverEvent[], currentNode: LifecycleNode) => { + return nodes.reduce((flattenedEvents: ResolverEvent[], currentNode: ResolverLifecycleNode) => { if (currentNode.lifecycle && currentNode.lifecycle.length > 0) { flattenedEvents.push(...currentNode.lifecycle); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index 7a3e1fc591e82..e60e5087c30a9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -9,7 +9,11 @@ import { parentEntityId, isProcessStart, } from '../../../../../common/endpoint/models/event'; -import { ChildNode, ResolverEvent, ResolverChildren } from '../../../../../common/endpoint/types'; +import { + ResolverChildNode, + ResolverEvent, + ResolverChildren, +} from '../../../../../common/endpoint/types'; import { PaginationBuilder } from './pagination'; import { createChild } from './node'; @@ -17,7 +21,7 @@ import { createChild } from './node'; * This class helps construct the children structure when building a resolver tree. */ export class ChildrenNodesHelper { - private readonly cache: Map = new Map(); + private readonly cache: Map = new Map(); constructor(private readonly rootID: string) { this.cache.set(rootID, createChild(rootID)); @@ -27,7 +31,7 @@ export class ChildrenNodesHelper { * Constructs a ResolverChildren response based on the children that were previously add. */ getNodes(): ResolverChildren { - const cacheCopy: Map = new Map(this.cache); + const cacheCopy: Map = new Map(this.cache); const rootNode = cacheCopy.get(this.rootID); let rootNextChild = null; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index d448649ae447b..0af2fca7106be 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -10,7 +10,7 @@ import { ResolverRelatedEvents, ResolverAncestry, ResolverRelatedAlerts, - LifecycleNode, + ResolverLifecycleNode, ResolverEvent, } from '../../../../../common/endpoint/types'; import { @@ -143,7 +143,7 @@ export class Fetcher { return tree; } - private async getNode(entityID: string): Promise { + private async getNode(entityID: string): Promise { const query = new LifecycleQuery(this.eventsIndexPattern, this.endpointID); const results = await query.search(this.client, entityID); if (results.length === 0) { @@ -186,7 +186,7 @@ export class Fetcher { // bucket the start and end events together for a single node const ancestryNodes = results.reduce( - (nodes: Map, ancestorEvent: ResolverEvent) => { + (nodes: Map, ancestorEvent: ResolverEvent) => { const nodeId = entityId(ancestorEvent); let node = nodes.get(nodeId); if (!node) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts index 58aa9efc1fc56..57a2ebfcc1792 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts @@ -7,10 +7,10 @@ import { ResolverEvent, ResolverAncestry, - LifecycleNode, + ResolverLifecycleNode, ResolverRelatedEvents, ResolverTree, - ChildNode, + ResolverChildNode, ResolverRelatedAlerts, } from '../../../../../common/endpoint/types'; @@ -49,7 +49,7 @@ export function createRelatedAlerts( * * @param entityID the entity_id of the child */ -export function createChild(entityID: string): ChildNode { +export function createChild(entityID: string): ResolverChildNode { const lifecycle = createLifecycle(entityID, []); return { ...lifecycle, @@ -70,7 +70,10 @@ export function createAncestry(): ResolverAncestry { * @param id the entity_id that these lifecycle nodes should have * @param lifecycle an array of lifecycle events */ -export function createLifecycle(entityID: string, lifecycle: ResolverEvent[]): LifecycleNode { +export function createLifecycle( + entityID: string, + lifecycle: ResolverEvent[] +): ResolverLifecycleNode { return { entityID, lifecycle }; } 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 09acf4fe1ef68..fe592aadb37a5 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 @@ -136,14 +136,14 @@ export const ThresholdExpression = ({ ) : null} 0 || !threshold[i]} + isInvalid={errors[`threshold${i}`]?.length > 0 || !threshold[i]} error={errors[`threshold${i}`]} > 0 || !threshold[i]} + isInvalid={errors[`threshold${i}`]?.length > 0 || !threshold[i]} onChange={(e) => { const { value } = e.target; const thresholdVal = value !== '' ? parseFloat(value) : undefined; diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index bc209e2bb4925..0877fdc949dc4 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -82,6 +82,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'localhost', 'some.non.existent.com', ])}`, + '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', `--xpack.actions.preconfigured=${JSON.stringify({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts index 912b0dc339a21..b8963d72ead50 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts @@ -17,8 +17,8 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) describe('actionType not enabled', () => { // loads action PREWRITTEN_ACTION_ID with actionType DISABLED_ACTION_TYPE - before(() => esArchiver.load('alerting')); - after(() => esArchiver.unload('alerting')); + before(() => esArchiver.load('actions')); + after(() => esArchiver.unload('actions')); it('should handle create action with disabled actionType request appropriately', async () => { const response = await supertest.post(`/api/actions/action`).set('kbn-xsrf', 'foo').send({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index a0c4da361bd38..2fc35ddaa3c61 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -26,5 +26,6 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); + loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts new file mode 100644 index 0000000000000..fc61f59d129d7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { getUrlPrefix } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('alerts'); + }); + + after(async () => { + await esArchiver.unload('alerts'); + }); + + it('7.9.0 migrates the `alerting` consumer to be the `alerts`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('alerts'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index 67b828b8df30e..eeca8ee54e32f 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -6,8 +6,8 @@ import _ from 'lodash'; import expect from '@kbn/expect'; import { - ChildNode, - LifecycleNode, + ResolverChildNode, + ResolverLifecycleNode, ResolverAncestry, ResolverEvent, ResolverRelatedEvents, @@ -35,7 +35,7 @@ import { Options, GeneratedTrees } from '../../services/resolver'; * @param node a lifecycle node containing the start and end events for a node * @param nodeMap a map of entity_ids to nodes to look for the passed in `node` */ -const expectLifecycleNodeInMap = (node: LifecycleNode, nodeMap: Map) => { +const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map) => { const genNode = nodeMap.get(node.entityID); expect(genNode).to.be.ok(); compareArrays(genNode!.lifecycle, node.lifecycle, true); @@ -49,7 +49,11 @@ const expectLifecycleNodeInMap = (node: LifecycleNode, nodeMap: Map { +const verifyAncestry = ( + ancestors: ResolverLifecycleNode[], + tree: Tree, + verifyLastParent: boolean +) => { // group the ancestors by their entity_id mapped to a lifecycle node const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); // group by parent entity_id @@ -97,7 +101,7 @@ const verifyAncestry = (ancestors: LifecycleNode[], tree: Tree, verifyLastParent * * @param ancestors an array of ancestor nodes */ -const retrieveDistantAncestor = (ancestors: LifecycleNode[]) => { +const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { // group the ancestors by their entity_id mapped to a lifecycle node const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); let node = ancestors[0]; @@ -124,7 +128,7 @@ const retrieveDistantAncestor = (ancestors: LifecycleNode[]) => { * @param childrenPerParent an optional number to compare that there are a certain number of children for each parent */ const verifyChildren = ( - children: ChildNode[], + children: ResolverChildNode[], tree: Tree, numberOfParents?: number, childrenPerParent?: number @@ -210,7 +214,7 @@ const verifyStats = ( * @param categories the related event info used when generating the resolver tree */ const verifyLifecycleStats = ( - nodes: LifecycleNode[], + nodes: ResolverLifecycleNode[], categories: RelatedEventInfo[], relatedAlerts: number ) => { diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts index bcdd3d1f82e7d..29ead0db1c634 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts @@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); // trigger drilldown action by clicking on a pie and picking drilldown action by it's name - await pieChart.filterOnPieSlice('40,000'); + await pieChart.clickOnPieSlice('40,000'); await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); const href = await dashboardDrilldownPanelActions.getActionHrefByText( diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts new file mode 100644 index 0000000000000..12363f8800c28 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const ACTION_ID = 'ACTION_EXPLORE_DATA_CHART'; +const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const drilldowns = getService('dashboardDrilldownsManage'); + const { dashboard, discover, common, timePicker } = getPageObjects([ + 'dashboard', + 'discover', + 'common', + 'timePicker', + ]); + const testSubjects = getService('testSubjects'); + const pieChart = getService('pieChart'); + const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); + const filterBar = getService('filterBar'); + const browser = getService('browser'); + + describe('Explore underlying data - chart action', () => { + describe('value click action', () => { + it('action exists in chart click popup menu', async () => { + await common.navigateToApp('dashboard'); + await dashboard.preserveCrossAppState(); + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await pieChart.clickOnPieSlice('160,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); + + it('action is a link element', async () => { + const actionElement = await testSubjects.find(ACTION_TEST_SUBJ); + const tag = await actionElement.getTagName(); + const href = await actionElement.getAttribute('href'); + + expect(tag.toLowerCase()).to.be('a'); + expect(typeof href).to.be('string'); + expect(href.length > 5).to.be(true); + }); + + it('navigates to Discover app on action click carrying over pie slice filter', async () => { + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); + await discover.waitForDiscoverAppOnScreen(); + await filterBar.hasFilter('memory', '160,000 to 200,000'); + const filterCount = await filterBar.getFilterCount(); + + expect(filterCount).to.be(1); + }); + }); + + describe('brush action', () => { + let originalTimeRangeDurationHours: number | undefined; + + it('action exists in chart brush popup menu', async () => { + await common.navigateToApp('dashboard'); + await dashboard.preserveCrossAppState(); + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_AREA_CHART_NAME); + + originalTimeRangeDurationHours = await timePicker.getTimeDurationInHours(); + const areaChart = await testSubjects.find('visualizationLoader'); + await browser.dragAndDrop( + { + location: areaChart, + offset: { + x: -100, + y: 0, + }, + }, + { + location: areaChart, + offset: { + x: 100, + y: 0, + }, + } + ); + + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); + + it('navigates to Discover on click carrying over brushed time range', async () => { + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); + await discover.waitForDiscoverAppOnScreen(); + const newTimeRangeDurationHours = await timePicker.getTimeDurationInHours(); + + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours as number); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts index 24d6e820ac0eb..fedc83a2f81c7 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; const ACTION_ID = 'ACTION_EXPLORE_DATA'; -const EXPLORE_RAW_DATA_ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; +const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; export default function ({ getService, getPageObjects }: FtrProviderContext) { const drilldowns = getService('dashboardDrilldownsManage'); @@ -24,31 +24,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('Explore underlying data - panel action', function () { - before(async () => { - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' }); + before( + 'change default index pattern to verify action navigates to correct index pattern', + async () => { + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' }); + } + ); + + before('start on Dashboard landing page', async () => { await common.navigateToApp('dashboard'); await dashboard.preserveCrossAppState(); }); - after(async () => { + after('set back default index pattern', async () => { await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); }); + after('clean-up custom time range on panel', async () => { + await common.navigateToApp('dashboard'); + await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await panelActions.openContextMenu(); + await panelActionsTimeRange.clickTimeRangeActionInContextMenu(); + await panelActionsTimeRange.clickRemovePerPanelTimeRangeButton(); + await dashboard.saveDashboard('Dashboard with Pie Chart'); + }); + it('action exists in panel context menu', async () => { await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); await panelActions.openContextMenu(); - await testSubjects.existOrFail(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); }); it('is a link element', async () => { - const actionElement = await testSubjects.find(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + const actionElement = await testSubjects.find(ACTION_TEST_SUBJ); const tag = await actionElement.getTagName(); expect(tag.toLowerCase()).to.be('a'); }); it('navigates to Discover app to index pattern of the panel on action click', async () => { - await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); await discover.waitForDiscoverAppOnScreen(); const el = await testSubjects.find('indexPattern-switch-link'); @@ -71,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.saveDashboard('Dashboard with Pie Chart'); await panelActions.openContextMenu(); - await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); await discover.waitForDiscoverAppOnScreen(); const text = await timePicker.getShowDatesButtonText(); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts index 19d85ad0e448f..4cdb33c06947f 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts @@ -24,5 +24,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_drilldowns')); loadTestFile(require.resolve('./explore_data_panel_action')); + loadTestFile(require.resolve('./explore_data_chart_action')); }); } diff --git a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts index 3483ddf769e5f..bf2a4192af543 100644 --- a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts +++ b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts @@ -12,15 +12,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const aceEditor = getService('aceEditor'); const retry = getService('retry'); + const security = getService('security'); const editorTestSubjectSelector = 'searchProfilerEditor'; describe('Search Profiler Editor', () => { before(async () => { + await security.testUser.setRoles(['global_devtools_read']); await PageObjects.common.navigateToApp('searchProfiler'); expect(await testSubjects.exists('searchProfilerEditor')).to.be(true); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('correctly parses triple quotes in JSON', async () => { // The below inputs are written to work _with_ ace's autocomplete unlike console's unit test // counterparts in src/legacy/core_plugins/console/public/tests/src/editor.test.js diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index b6f0fdce8b289..eeb9bc9b84450 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -13,7 +13,7 @@ export default function statusPageFunctonalTests({ const PageObjects = getPageObjects(['security', 'statusPage', 'home']); describe('Status Page', function () { - this.tags('includeFirefox'); + this.tags(['skipCloud', 'includeFirefox']); before(async () => await esArchiver.load('empty_kibana')); after(async () => await esArchiver.unload('empty_kibana')); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index d5e3f82878d6b..14e05d21b8753 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -231,6 +231,17 @@ export default async function ({ readConfigFile }) { ], }, + global_devtools_read: { + kibana: [ + { + feature: { + dev_tools: ['read'], + }, + spaces: ['*'], + }, + ], + }, + //Kibana feature privilege isn't specific to advancedSetting. It can be anything. https://github.com/elastic/kibana/issues/35965 test_api_keys: { elasticsearch: { diff --git a/x-pack/test/functional/es_archives/alerting/data.json b/x-pack/test/functional/es_archives/actions/data.json similarity index 100% rename from x-pack/test/functional/es_archives/alerting/data.json rename to x-pack/test/functional/es_archives/actions/data.json diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json new file mode 100644 index 0000000000000..3703473606ea2 --- /dev/null +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -0,0 +1,41 @@ +{ + "type": "doc", + "value": { + "id": "alert:74f3e6d7-b7bb-477d-ac28-92ee22728e6e", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + ], + "alertTypeId": "example.always-firing", + "apiKey": "QIUT8u0/kbOakEHSj50jDpVR90MrqOxanEscboYOoa8PxQvcA5jfHash+fqH3b+KNjJ1LpnBcisGuPkufY9j1e32gKzwGZV5Bfys87imHvygJvIM8uKiFF8bQ8Y4NTaxOJO9fAmZPrFy07ZcQMCAQz+DUTgBFqs=", + "apiKeyOwner": "elastic", + "consumer": "alerting", + "createdAt": "2020-06-17T15:35:38.497Z", + "createdBy": "elastic", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "always-firing-alert", + "params": { + }, + "schedule": { + "interval": "1m" + }, + "scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4", + "tags": [ + ], + "throttle": null, + "updatedBy": "elastic" + }, + "migrationVersion": { + "alert": "7.8.0" + }, + "references": [ + ], + "type": "alert", + "updated_at": "2020-06-17T15:35:39.839Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/alerts/mappings.json b/x-pack/test/functional/es_archives/alerts/mappings.json new file mode 100644 index 0000000000000..287d9a79a68cf --- /dev/null +++ b/x-pack/test/functional/es_archives/alerts/mappings.json @@ -0,0 +1,345 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "todo": "082a2cc96a590268344d5cd74c159ac4", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "alert": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/services/dashboard/panel_time_range.ts b/x-pack/test/functional/services/dashboard/panel_time_range.ts index 6a91a6ff0584b..f71e8284c30d9 100644 --- a/x-pack/test/functional/services/dashboard/panel_time_range.ts +++ b/x-pack/test/functional/services/dashboard/panel_time_range.ts @@ -52,5 +52,11 @@ export function DashboardPanelTimeRangeProvider({ getService }: FtrProviderConte const button = await this.findModalTestSubject('addPerPanelTimeRangeButton'); await button.click(); } + + public async clickRemovePerPanelTimeRangeButton() { + log.debug('clickRemovePerPanelTimeRangeButton'); + const button = await this.findModalTestSubject('removePerPanelTimeRangeButton'); + await button.click(); + } })(); } diff --git a/x-pack/test/reporting_api_integration/services.ts b/x-pack/test/reporting_api_integration/services.ts index dadb466d45982..85f5a98c69b2e 100644 --- a/x-pack/test/reporting_api_integration/services.ts +++ b/x-pack/test/reporting_api_integration/services.ts @@ -7,8 +7,7 @@ import expect from '@kbn/expect'; import * as Rx from 'rxjs'; import { filter, first, mapTo, switchMap, timeout } from 'rxjs/operators'; -// @ts-ignore no module definition -import { indexTimestamp } from '../../plugins/reporting/server/lib/esqueue/helpers/index_timestamp'; +import { indexTimestamp } from '../../plugins/reporting/server/lib/store/index_timestamp'; import { services as xpackServices } from '../functional/services'; import { FtrProviderContext } from './ftr_provider_context'; diff --git a/yarn.lock b/yarn.lock index 93db6de88775c..60122f8b8cde7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12958,9 +12958,9 @@ element-resize-detector@^1.1.15: batch-processor "^1.0.0" elliptic@^6.0.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" - integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1"