diff --git a/dev_docs/assets/kibana_template_solution_nav.png b/dev_docs/assets/kibana_template_solution_nav.png new file mode 100644 index 00000000000000..4ffec1990ed0a4 Binary files /dev/null and b/dev_docs/assets/kibana_template_solution_nav.png differ diff --git a/dev_docs/assets/kibana_template_solution_nav_mobile.png b/dev_docs/assets/kibana_template_solution_nav_mobile.png new file mode 100644 index 00000000000000..487114f7a344af Binary files /dev/null and b/dev_docs/assets/kibana_template_solution_nav_mobile.png differ diff --git a/dev_docs/tutorials/kibana_page_template.mdx b/dev_docs/tutorials/kibana_page_template.mdx index aa38890a8ac9ec..d9605ac5643ba9 100644 --- a/dev_docs/tutorials/kibana_page_template.mdx +++ b/dev_docs/tutorials/kibana_page_template.mdx @@ -9,13 +9,13 @@ tags: ['kibana', 'dev', 'ui', 'tutorials'] `KibanaPageTemplate` is a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements and patterns. -Refer to EUI's documentation on [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) for constructing page layouts. +Refer to EUI's documentation on [**EuiPageTemplate**](https://elastic.github.io/eui/#/layout/page) for constructing page layouts. ## `isEmptyState` Use the `isEmptyState` prop for when there is no page content to show. For example, before the user has created something, when no search results are found, before data is populated, or when permissions aren't met. -The default empty state uses any `pageHeader` info provided to populate an [`EuiEmptyPrompt`](https://elastic.github.io/eui/#/display/empty-prompt) and uses the `centeredBody` template type. +The default empty state uses any `pageHeader` info provided to populate an [**EuiEmptyPrompt**](https://elastic.github.io/eui/#/display/empty-prompt) and uses the `centeredBody` template type. ```tsx + {...} + +``` + + +![Screenshot of Stack Management empty state with a provided solution navigation shown on the left, outlined in pink.](../assets/kibana_template_solution_nav.png) + +![Screenshots of Stack Management page in mobile view. Menu closed on the left, menu open on the right.](../assets/kibana_template_solution_nav_mobile.png) diff --git a/docs/developer/contributing/development-functional-tests.asciidoc b/docs/developer/contributing/development-functional-tests.asciidoc index 110704a8e569a9..f0041b85c14ebf 100644 --- a/docs/developer/contributing/development-functional-tests.asciidoc +++ b/docs/developer/contributing/development-functional-tests.asciidoc @@ -139,11 +139,14 @@ export default function (/* { providerAPI } */) { } ----------- -**Services**::: -Services are named singleton values produced by a Service Provider. Tests and other services can retrieve service instances by asking for them by name. All functionality except the mocha API is exposed via services. +**Service**::: +A Service is a named singleton created using a subclass of `FtrService`. Tests and other services can retrieve service instances by asking for them by name. All functionality except the mocha API is exposed via services. When you write your own functional tests check for existing services that help with the interactions you're looking to execute, and add new services for interactions which aren't already encoded in a service. + +**Service Providers**::: +For legacy purposes, and for when creating a subclass of `FtrService` is inconvenient, you can also create services using a "Service Provider". These are functions which which create service instances and return them. These instances are cached and provided to tests. Currently these providers may also return a Promise for the service instance, allowing the service to do some setup work before tests run. We expect to fully deprecate and remove support for async service providers in the near future and instead require that services use the `lifecycle` service to run setup before tests. Providers which return instances of classes other than `FtrService` will likely remain supported for as long as possible. **Page objects**::: -Page objects are a special type of service that encapsulate behaviors common to a particular page or plugin. When you write your own plugin, you’ll likely want to add a page object (or several) that describes the common interactions your tests need to execute. +Page objects are functionally equivalent to services, except they are loaded with a slightly different mechanism and generally defined separate from services. When you write your own functional tests you might want to write some of your services as Page objects, but it is not required. **Test Files**::: The `FunctionalTestRunner`'s primary purpose is to execute test files. These files export a Test Provider that is called with a Provider API but is not expected to return a value. Instead Test Providers define a suite using https://mochajs.org/#bdd[mocha's BDD interface]. diff --git a/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md b/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md new file mode 100644 index 00000000000000..0392cb7eaefb02 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [deepLinks](./kibana-plugin-core-public.app.deeplinks.md) + +## App.deepLinks property + +Input type for registering secondary in-app locations for an application. + +Deep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. + +Signature: + +```typescript +deepLinks?: AppDeepLink[]; +``` + +## Example + + +```ts +core.application.register({ + id: 'my_app', + title: 'Translated title', + keywords: ['translated keyword1', 'translated keyword2'], + deepLinks: [ + { id: 'sub1', title: 'Sub1', path: '/sub1', keywords: ['subpath1'] }, + { + id: 'sub2', + title: 'Sub2', + deepLinks: [ + { id: 'subsub', title: 'SubSub', path: '/sub2/sub', keywords: ['subpath2'] } + ] + } + ], + mount: () => { ... } +}) + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.app.keywords.md b/docs/development/core/public/kibana-plugin-core-public.app.keywords.md new file mode 100644 index 00000000000000..585df1b48c16e7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.keywords.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [keywords](./kibana-plugin-core-public.app.keywords.md) + +## App.keywords property + +Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. + +Signature: + +```typescript +keywords?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index 9a508f293d8e8c..721d9a2f121c73 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -19,12 +19,13 @@ export interface App | [capabilities](./kibana-plugin-core-public.app.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | | [category](./kibana-plugin-core-public.app.category.md) | AppCategory | The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | | [chromeless](./kibana-plugin-core-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | +| [deepLinks](./kibana-plugin-core-public.app.deeplinks.md) | AppDeepLink[] | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or deepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [defaultPath](./kibana-plugin-core-public.app.defaultpath.md) | string | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | | [euiIconType](./kibana-plugin-core-public.app.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [exactRoute](./kibana-plugin-core-public.app.exactroute.md) | boolean | If set to true, the application's route will only be checked against an exact match. Defaults to false. | | [icon](./kibana-plugin-core-public.app.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application | -| [meta](./kibana-plugin-core-public.app.meta.md) | AppMeta | Meta data for an application that represent additional information for the app. See [AppMeta](./kibana-plugin-core-public.appmeta.md) | +| [keywords](./kibana-plugin-core-public.app.keywords.md) | string[] | Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. | | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | A mount function called when the user navigates to this app's route. | | [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | | [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.meta.md b/docs/development/core/public/kibana-plugin-core-public.app.meta.md deleted file mode 100644 index 574fa11605aec9..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.app.meta.md +++ /dev/null @@ -1,43 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [meta](./kibana-plugin-core-public.app.meta.md) - -## App.meta property - -Meta data for an application that represent additional information for the app. See [AppMeta](./kibana-plugin-core-public.appmeta.md) - -Signature: - -```typescript -meta?: AppMeta; -``` - -## Remarks - -Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) for more details. - -## Example - - -```ts -core.application.register({ - id: 'my_app', - title: 'Translated title', - meta: { - keywords: ['translated keyword1', 'translated keyword2'], - searchDeepLinks: [ - { id: 'sub1', title: 'Sub1', path: '/sub1', keywords: ['subpath1'] }, - { - id: 'sub2', - title: 'Sub2', - searchDeepLinks: [ - { id: 'subsub', title: 'SubSub', path: '/sub2/sub', keywords: ['subpath2'] } - ] - } - ], - }, - mount: () => { ... } -}) - -``` - diff --git a/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md b/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md new file mode 100644 index 00000000000000..5aa951cffdcb54 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppDeepLink](./kibana-plugin-core-public.appdeeplink.md) + +## AppDeepLink type + +Input type for registering secondary in-app locations for an application. + +Deep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. + +Signature: + +```typescript +export declare type AppDeepLink = { + id: string; + title: string; + keywords?: string[]; + navLinkStatus?: AppNavLinkStatus; +} & ({ + path: string; + deepLinks?: AppDeepLink[]; +} | { + path?: string; + deepLinks: AppDeepLink[]; +}); +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md b/docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md deleted file mode 100644 index 13709df68e76a1..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMeta](./kibana-plugin-core-public.appmeta.md) > [keywords](./kibana-plugin-core-public.appmeta.keywords.md) - -## AppMeta.keywords property - -Keywords to represent this application - -Signature: - -```typescript -keywords?: string[]; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmeta.md b/docs/development/core/public/kibana-plugin-core-public.appmeta.md deleted file mode 100644 index a2b72f7ec799d2..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appmeta.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMeta](./kibana-plugin-core-public.appmeta.md) - -## AppMeta interface - -Input type for meta data for an application. - -Meta fields include `keywords` and `searchDeepLinks` Keywords is an array of string with which to associate the app, must include at least one unique string as an array. `searchDeepLinks` is an array of links that represent secondary in-app locations for the app. - -Signature: - -```typescript -export interface AppMeta -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [keywords](./kibana-plugin-core-public.appmeta.keywords.md) | string[] | Keywords to represent this application | -| [searchDeepLinks](./kibana-plugin-core-public.appmeta.searchdeeplinks.md) | AppSearchDeepLink[] | Array of links that represent secondary in-app locations for the app. | - diff --git a/docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md b/docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md deleted file mode 100644 index 7ec0bbaa4b418b..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMeta](./kibana-plugin-core-public.appmeta.md) > [searchDeepLinks](./kibana-plugin-core-public.appmeta.searchdeeplinks.md) - -## AppMeta.searchDeepLinks property - -Array of links that represent secondary in-app locations for the app. - -Signature: - -```typescript -searchDeepLinks?: AppSearchDeepLink[]; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md b/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md deleted file mode 100644 index 29aad675fb105d..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppSearchDeepLink](./kibana-plugin-core-public.appsearchdeeplink.md) - -## AppSearchDeepLink type - -Input type for registering secondary in-app locations for an application. - -Deep links must include at least one of `path` or `searchDeepLinks`. A deep link that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. - -Signature: - -```typescript -export declare type AppSearchDeepLink = { - id: string; - title: string; -} & ({ - path: string; - searchDeepLinks?: AppSearchDeepLink[]; - keywords?: string[]; -} | { - path?: string; - searchDeepLinks: AppSearchDeepLink[]; - keywords?: string[]; -}); -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index 55672d9339f615..d7b12d4b707010 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 3972f737f66183..6239279f275d1c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -37,7 +37,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppLeaveDefaultAction](./kibana-plugin-core-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) to execute the default behaviour when leaving the application.See | | [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | -| [AppMeta](./kibana-plugin-core-public.appmeta.md) | Input type for meta data for an application.Meta fields include keywords and searchDeepLinks Keywords is an array of string with which to associate the app, must include at least one unique string as an array. searchDeepLinks is an array of links that represent secondary in-app locations for the app. | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | | [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | @@ -144,10 +143,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | +| [AppDeepLink](./kibana-plugin-core-public.appdeeplink.md) | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or deepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [AppLeaveAction](./kibana-plugin-core-public.appleaveaction.md) | Possible actions to return from a [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md)See [AppLeaveConfirmAction](./kibana-plugin-core-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-core-public.appleavedefaultaction.md) | | [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return confirm to prompt a message to the user before leaving the page, or default to keep the default behavior (doing nothing).See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for detailed usage examples. | | [AppMount](./kibana-plugin-core-public.appmount.md) | A mount function called when the user navigates to this app's route. | -| [AppSearchDeepLink](./kibana-plugin-core-public.appsearchdeeplink.md) | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or searchDeepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [AppUnmount](./kibana-plugin-core-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-core-public.appupdater.md). | | [AppUpdater](./kibana-plugin-core-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | @@ -161,9 +160,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [NavType](./kibana-plugin-core-public.navtype.md) | | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | +| [PublicAppDeepLinkInfo](./kibana-plugin-core-public.publicappdeeplinkinfo.md) | Public information about a registered app's [deepLinks](./kibana-plugin-core-public.appdeeplink.md) | | [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | -| [PublicAppMetaInfo](./kibana-plugin-core-public.publicappmetainfo.md) | Public information about a registered app's [keywords](./kibana-plugin-core-public.appmeta.md) | -| [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) | Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [ResolveDeprecationResponse](./kibana-plugin-core-public.resolvedeprecationresponse.md) | | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md new file mode 100644 index 00000000000000..d3a6a4de905fdf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppDeepLinkInfo](./kibana-plugin-core-public.publicappdeeplinkinfo.md) + +## PublicAppDeepLinkInfo type + +Public information about a registered app's [deepLinks](./kibana-plugin-core-public.appdeeplink.md) + +Signature: + +```typescript +export declare type PublicAppDeepLinkInfo = Omit & { + deepLinks: PublicAppDeepLinkInfo[]; + keywords: string[]; + navLinkStatus: AppNavLinkStatus; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md index 9f45a06935fe4c..a5563eae83563f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -9,10 +9,11 @@ Public information about a registered [application](./kibana-plugin-core-public. Signature: ```typescript -export declare type PublicAppInfo = Omit & { +export declare type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; - meta: PublicAppMetaInfo; + keywords: string[]; + deepLinks: PublicAppDeepLinkInfo[]; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md deleted file mode 100644 index 3ef0460aec4670..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppMetaInfo](./kibana-plugin-core-public.publicappmetainfo.md) - -## PublicAppMetaInfo type - -Public information about a registered app's [keywords](./kibana-plugin-core-public.appmeta.md) - -Signature: - -```typescript -export declare type PublicAppMetaInfo = Omit & { - keywords: string[]; - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; -}; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md deleted file mode 100644 index e88cdb7d55edd6..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) - -## PublicAppSearchDeepLinkInfo type - -Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) - -Signature: - -```typescript -export declare type PublicAppSearchDeepLinkInfo = Omit & { - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; - keywords: string[]; -}; -``` diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index e4150fc280096c..45d24bfb5a7e4f 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -1,6 +1,5 @@ :ems-docker-repo: docker.elastic.co/elastic-maps-service/elastic-maps-server-ubi8 :ems-docker-image: {ems-docker-repo}:{version} -:hosted-ems: Elastic Maps Server [role="xpack"] [[maps]] diff --git a/package.json b/package.json index 5737bce303e09c..73f3e5585faf74 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "@kbn/config": "link:bazel-bin/packages/kbn-config/npm_module", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto/npm_module", + "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module", "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n/npm_module", "@kbn/interpreter": "link:packages/kbn-interpreter", "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index ec8252cb6144da..43528e0ae41629 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -24,6 +24,7 @@ filegroup( "//packages/kbn-i18n:build", "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", + "//packages/kbn-mapbox-gl:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-securitysolution-list-constants:build", "//packages/kbn-securitysolution-io-ts-types:build", diff --git a/packages/kbn-mapbox-gl/BUILD.bazel b/packages/kbn-mapbox-gl/BUILD.bazel new file mode 100644 index 00000000000000..7d7186068832ec --- /dev/null +++ b/packages/kbn-mapbox-gl/BUILD.bazel @@ -0,0 +1,84 @@ + +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-mapbox-gl" +PKG_REQUIRE_NAME = "@kbn/mapbox-gl" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "@npm//@mapbox/mapbox-gl-rtl-text", + "@npm//file-loader", + "@npm//mapbox-gl", +] + +TYPES_DEPS = [ + "@npm//@types/mapbox-gl", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":tsc"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-mapbox-gl/README.md b/packages/kbn-mapbox-gl/README.md new file mode 100644 index 00000000000000..c1fdea491feaae --- /dev/null +++ b/packages/kbn-mapbox-gl/README.md @@ -0,0 +1,3 @@ +# @kbn/mapbox-gl + +Default instantiation for mapbox-gl. \ No newline at end of file diff --git a/packages/kbn-mapbox-gl/package.json b/packages/kbn-mapbox-gl/package.json new file mode 100644 index 00000000000000..9de88dac54a5ab --- /dev/null +++ b/packages/kbn-mapbox-gl/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/mapbox-gl", + "version": "1.0.0", + "private": true, + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts" +} diff --git a/packages/kbn-mapbox-gl/src/index.ts b/packages/kbn-mapbox-gl/src/index.ts new file mode 100644 index 00000000000000..117b874a28ffbd --- /dev/null +++ b/packages/kbn-mapbox-gl/src/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './typings'; +import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; +// @ts-expect-error +import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; +// @ts-expect-error +import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; +import 'mapbox-gl/dist/mapbox-gl.css'; + +mapboxgl.workerUrl = mbWorkerUrl; +mapboxgl.setRTLTextPlugin(mbRtlPlugin); + +export { mapboxgl }; diff --git a/packages/kbn-mapbox-gl/src/typings.ts b/packages/kbn-mapbox-gl/src/typings.ts new file mode 100644 index 00000000000000..0cc6908aca4284 --- /dev/null +++ b/packages/kbn-mapbox-gl/src/typings.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Mapbox-gl doesn't declare this type. +declare module 'mapbox-gl/dist/mapbox-gl-csp'; diff --git a/packages/kbn-mapbox-gl/tsconfig.json b/packages/kbn-mapbox-gl/tsconfig.json new file mode 100644 index 00000000000000..cf1cca0f5a0fd1 --- /dev/null +++ b/packages/kbn-mapbox-gl/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": true, + "outDir": "./target", + "declaration": true, + "declarationMap": true, + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-mapbox-gl/src", + "types": [] + }, + "include": [ + "src/**/*", + ] +} diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 7f2d0768a1fb9d..a585a0fc7542f5 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -9,7 +9,7 @@ pageLoadAssetSize: charts: 195358 cloud: 21076 console: 46091 - core: 414000 + core: 432925 crossClusterReplication: 65408 dashboard: 374194 dashboardEnhanced: 65646 diff --git a/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts index 1aa5df1105f460..2d05d5bba5ff6f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts @@ -12,6 +12,7 @@ import { loadTracer } from '../load_tracer'; import { createAsyncInstance, isAsyncInstance } from './async_instance'; import { Providers } from './read_provider_spec'; import { createVerboseInstance } from './verbose_instance'; +import { GenericFtrService } from '../../public_types'; export class ProviderCollection { private readonly instances = new Map(); @@ -58,12 +59,19 @@ export class ProviderCollection { } public invokeProviderFn(provider: (args: any) => any) { - return provider({ + const ctx = { getService: this.getService, hasService: this.hasService, getPageObject: this.getPageObject, getPageObjects: this.getPageObjects, - }); + }; + + if (provider.prototype instanceof GenericFtrService) { + const Constructor = (provider as any) as new (ctx: any) => any; + return new Constructor(ctx); + } + + return provider(ctx); } private findProvider(type: string, name: string) { diff --git a/packages/kbn-test/src/functional_test_runner/public_types.ts b/packages/kbn-test/src/functional_test_runner/public_types.ts index 915cb34f6ffe50..4a30744c09b516 100644 --- a/packages/kbn-test/src/functional_test_runner/public_types.ts +++ b/packages/kbn-test/src/functional_test_runner/public_types.ts @@ -13,7 +13,7 @@ import { Test, Suite } from './fake_mocha_types'; export { Lifecycle, Config, FailureMetadata }; -interface AsyncInstance { +export interface AsyncInstance { /** * Services that are initialized async are not ready before the tests execute, so you might need * to call `init()` and await the promise it returns before interacting with the service @@ -39,7 +39,11 @@ export type ProvidedType any> = MaybeAsyncInstance * promise types into the async instances that other providers will receive. */ type ProvidedTypeMap = { - [K in keyof T]: T[K] extends (...args: any[]) => any ? ProvidedType : unknown; + [K in keyof T]: T[K] extends new (...args: any[]) => infer X + ? X + : T[K] extends (...args: any[]) => any + ? ProvidedType + : unknown; }; export interface GenericFtrProviderContext< @@ -84,6 +88,10 @@ export interface GenericFtrProviderContext< loadTestFile(path: string): void; } +export class GenericFtrService> { + constructor(protected readonly ctx: ProviderContext) {} +} + export interface FtrConfigProviderContext { log: ToolingLog; readConfigFile(path: string): Promise; diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 76b9c7a73d3bd1..2e2f1cad49f199 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -75,7 +75,10 @@ describe('#setup()', () => { const pluginId = Symbol('plugin'); const updater$ = new BehaviorSubject((app) => ({})); setup.register(pluginId, createApp({ id: 'app1', updater$ })); - setup.register(pluginId, createApp({ id: 'app2' })); + setup.register( + pluginId, + createApp({ id: 'app2', deepLinks: [{ id: 'subapp1', title: 'Subapp', path: '/subapp' }] }) + ); const { applications$ } = await service.start(startDeps); let applications = await applications$.pipe(take(1)).toPromise(); @@ -92,6 +95,11 @@ describe('#setup()', () => { id: 'app2', navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, + deepLinks: [ + expect.objectContaining({ + navLinkStatus: AppNavLinkStatus.hidden, + }), + ], }) ); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 4a93c98205b842..bbfea61220b513 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -19,6 +19,7 @@ import { AppRouter } from './ui'; import { Capabilities, CapabilitiesService } from './capabilities'; import { App, + AppDeepLink, AppLeaveHandler, AppMount, AppNavLinkStatus, @@ -166,6 +167,7 @@ export class ApplicationService { ...appProps, status: app.status ?? AppStatus.accessible, navLinkStatus: app.navLinkStatus ?? AppNavLinkStatus.default, + deepLinks: populateDeepLinkDefaults(appProps.deepLinks), }); if (updater$) { registerStatusUpdater(app.id, updater$); @@ -392,3 +394,12 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => { ...changes, }; }; + +const populateDeepLinkDefaults = (deepLinks?: AppDeepLink[]): AppDeepLink[] => { + if (!deepLinks) return []; + return deepLinks.map((deepLink) => ({ + ...deepLink, + navLinkStatus: deepLink.navLinkStatus ?? AppNavLinkStatus.default, + deepLinks: populateDeepLinkDefaults(deepLink.deepLinks), + })); +}; diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 1e9a91717e81ae..68e1991646afbd 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -18,8 +18,7 @@ export type { AppMountParameters, AppUpdatableFields, AppUpdater, - AppMeta, - AppSearchDeepLink, + AppDeepLink, ApplicationSetup, ApplicationStart, AppLeaveHandler, @@ -29,8 +28,7 @@ export type { AppLeaveConfirmAction, NavigateToAppOptions, PublicAppInfo, - PublicAppMetaInfo, - PublicAppSearchDeepLinkInfo, + PublicAppDeepLinkInfo, // Internal types InternalApplicationSetup, InternalApplicationStart, diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 24f46752f28e58..ffc41955360bdf 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -63,7 +63,7 @@ export enum AppNavLinkStatus { */ export type AppUpdatableFields = Pick< App, - 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'meta' + 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'deepLinks' >; /** @@ -211,106 +211,92 @@ export interface App { */ exactRoute?: boolean; + /** Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. */ + keywords?: string[]; + /** - * Meta data for an application that represent additional information for the app. - * See {@link AppMeta} + * Input type for registering secondary in-app locations for an application. * - * @remarks - * Used to populate navigational search results (where available). - * Can be updated using the {@link App.updater$} observable. See {@link PublicAppSearchDeepLinkInfo} for more details. + * Deep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path` + * represents a topological level in the application's hierarchy, but does not have a destination URL that is + * user-accessible. * * @example * ```ts * core.application.register({ * id: 'my_app', * title: 'Translated title', - * meta: { - * keywords: ['translated keyword1', 'translated keyword2'], - * searchDeepLinks: [ - * { id: 'sub1', title: 'Sub1', path: '/sub1', keywords: ['subpath1'] }, + * keywords: ['translated keyword1', 'translated keyword2'], + * deepLinks: [ + * { + * id: 'sub1', + * title: 'Sub1', + * path: '/sub1', + * keywords: ['subpath1'], + * }, * { * id: 'sub2', * title: 'Sub2', - * searchDeepLinks: [ - * { id: 'subsub', title: 'SubSub', path: '/sub2/sub', keywords: ['subpath2'] } - * ] - * } + * deepLinks: [ + * { + * id: 'subsub', + * title: 'SubSub', + * path: '/sub2/sub', + * keywords: ['subpath2'], + * }, + * ], + * }, * ], - * }, * mount: () => { ... } * }) * ``` */ - meta?: AppMeta; -} - -/** - * Input type for meta data for an application. - * - * Meta fields include `keywords` and `searchDeepLinks` - * Keywords is an array of string with which to associate the app, must include at least one unique string as an array. - * `searchDeepLinks` is an array of links that represent secondary in-app locations for the app. - * @public - */ -export interface AppMeta { - /** Keywords to represent this application */ - keywords?: string[]; - /** Array of links that represent secondary in-app locations for the app. */ - searchDeepLinks?: AppSearchDeepLink[]; + deepLinks?: AppDeepLink[]; } /** - * Public information about a registered app's {@link AppMeta | keywords } + * Public information about a registered app's {@link AppDeepLink | deepLinks} * * @public */ -export type PublicAppMetaInfo = Omit & { - keywords: string[]; - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; -}; - -/** - * Public information about a registered app's {@link AppSearchDeepLink | searchDeepLinks} - * - * @public - */ -export type PublicAppSearchDeepLinkInfo = Omit< - AppSearchDeepLink, - 'searchDeepLinks' | 'keywords' +export type PublicAppDeepLinkInfo = Omit< + AppDeepLink, + 'deepLinks' | 'keywords' | 'navLinkStatus' > & { - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; + deepLinks: PublicAppDeepLinkInfo[]; keywords: string[]; + navLinkStatus: AppNavLinkStatus; }; /** * Input type for registering secondary in-app locations for an application. * - * Deep links must include at least one of `path` or `searchDeepLinks`. A deep link that does not have a `path` + * Deep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path` * represents a topological level in the application's hierarchy, but does not have a destination URL that is * user-accessible. * @public */ -export type AppSearchDeepLink = { +export type AppDeepLink = { /** Identifier to represent this sublink, should be unique for this application */ id: string; /** Title to label represent this deep link */ title: string; + /** Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. */ + keywords?: string[]; + /** Optional status of the chrome navigation, defaults to `hidden` */ + navLinkStatus?: AppNavLinkStatus; } & ( | { /** URL path to access this link, relative to the application's appRoute. */ path: string; /** Optional array of links that are 'underneath' this section in the hierarchy */ - searchDeepLinks?: AppSearchDeepLink[]; - /** Optional keywords to match with in deep links search for the page at the path */ - keywords?: string[]; + deepLinks?: AppDeepLink[]; } | { /** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */ path?: string; /** Array links that are 'underneath' this section in this hierarchy. */ - searchDeepLinks: AppSearchDeepLink[]; - /** Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. */ - keywords?: string[]; + deepLinks: AppDeepLink[]; } ); @@ -319,12 +305,13 @@ export type AppSearchDeepLink = { * * @public */ -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { // remove optional on fields populated with default values status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; - meta: PublicAppMetaInfo; + keywords: string[]; + deepLinks: PublicAppDeepLinkInfo[]; }; /** diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts index 28824867234ff9..ef4a06707d6664 100644 --- a/src/core/public/application/utils/get_app_info.test.ts +++ b/src/core/public/application/utils/get_app_info.test.ts @@ -32,24 +32,20 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - meta: { - keywords: [], - searchDeepLinks: [], - }, + keywords: [], + deepLinks: [], }); }); - it('populates default values for nested searchDeepLinks', () => { + it('populates default values for nested deepLinks', () => { const app = createApp({ - meta: { - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - searchDeepLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], - }, - ], - }, + deepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + deepLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], + }, + ], }); const info = getAppInfo(app); @@ -59,25 +55,23 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - meta: { - keywords: [], - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - keywords: [], - searchDeepLinks: [ - { - id: 'sub-sub-id', - title: 'sub-sub-title', - path: '/sub-sub', - keywords: [], - searchDeepLinks: [], // default empty array added - }, - ], - }, - ], - }, + keywords: [], + deepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + keywords: [], + deepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + keywords: [], + deepLinks: [], // default empty array added + }, + ], + }, + ], }); }); @@ -110,22 +104,20 @@ describe('getAppInfo', () => { it('adds default meta fields to sublinks when needed', () => { const app = createApp({ - meta: { - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - searchDeepLinks: [ - { - id: 'sub-sub-id', - title: 'sub-sub-title', - path: '/sub-sub', - keywords: ['sub sub'], - }, - ], - }, - ], - }, + deepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + deepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + keywords: ['sub sub'], + }, + ], + }, + ], }); const info = getAppInfo(app); @@ -135,25 +127,23 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - meta: { - keywords: [], - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - keywords: [], // default empty array - searchDeepLinks: [ - { - id: 'sub-sub-id', - title: 'sub-sub-title', - path: '/sub-sub', - keywords: ['sub sub'], - searchDeepLinks: [], - }, - ], - }, - ], - }, + keywords: [], + deepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + keywords: [], // default empty array + deepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + keywords: ['sub sub'], + deepLinks: [], + }, + ], + }, + ], }); }); }); diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts index ca1e8ac8076463..4c94e24f501bc4 100644 --- a/src/core/public/application/utils/get_app_info.ts +++ b/src/core/public/application/utils/get_app_info.ts @@ -10,9 +10,9 @@ import { App, AppNavLinkStatus, AppStatus, - AppSearchDeepLink, + AppDeepLink, PublicAppInfo, - PublicAppSearchDeepLinkInfo, + PublicAppDeepLinkInfo, } from '../types'; export function getAppInfo(app: App): PublicAppInfo { @@ -28,29 +28,27 @@ export function getAppInfo(app: App): PublicAppInfo { status: app.status!, navLinkStatus, appRoute: app.appRoute!, - meta: { - keywords: app.meta?.keywords ?? [], - searchDeepLinks: getSearchDeepLinkInfos(app, app.meta?.searchDeepLinks), - }, + keywords: app.keywords ?? [], + deepLinks: getDeepLinkInfos(app.deepLinks), }; } -function getSearchDeepLinkInfos( - app: App, - searchDeepLinks?: AppSearchDeepLink[] -): PublicAppSearchDeepLinkInfo[] { - if (!searchDeepLinks) { - return []; - } +function getDeepLinkInfos(deepLinks?: AppDeepLink[]): PublicAppDeepLinkInfo[] { + if (!deepLinks) return []; - return searchDeepLinks.map( - (rawDeepLink): PublicAppSearchDeepLinkInfo => { + return deepLinks.map( + (rawDeepLink): PublicAppDeepLinkInfo => { + const navLinkStatus = + rawDeepLink.navLinkStatus === AppNavLinkStatus.default + ? AppNavLinkStatus.hidden + : rawDeepLink.navLinkStatus!; return { id: rawDeepLink.id, title: rawDeepLink.title, path: rawDeepLink.path, keywords: rawDeepLink.keywords ?? [], - searchDeepLinks: getSearchDeepLinkInfos(app, rawDeepLink.searchDeepLinks), + navLinkStatus, + deepLinks: getDeepLinkInfos(rawDeepLink.deepLinks), }; } ); diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 41c4ff178d7378..db783d0028f075 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -17,10 +17,8 @@ const app = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, appRoute: `/app/some-id`, - meta: { - keywords: [], - searchDeepLinks: [], - }, + keywords: [], + deepLinks: [], ...props, }); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 129ae1c16f99b1..24b48683cdd937 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -89,13 +89,11 @@ export type { AppLeaveAction, AppLeaveDefaultAction, AppLeaveConfirmAction, - AppMeta, AppUpdatableFields, AppUpdater, - AppSearchDeepLink, + AppDeepLink, PublicAppInfo, - PublicAppMetaInfo, - PublicAppSearchDeepLinkInfo, + PublicAppDeepLinkInfo, NavigateToAppOptions, } from './application'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 31e7a1e2321dfc..9f0c5135e702fa 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -58,12 +58,13 @@ export interface App { capabilities?: Partial; category?: AppCategory; chromeless?: boolean; + deepLinks?: AppDeepLink[]; defaultPath?: string; euiIconType?: string; exactRoute?: boolean; icon?: string; id: string; - meta?: AppMeta; + keywords?: string[]; mount: AppMount; navLinkStatus?: AppNavLinkStatus; order?: number; @@ -85,6 +86,20 @@ export interface AppCategory { order?: number; } +// @public +export type AppDeepLink = { + id: string; + title: string; + keywords?: string[]; + navLinkStatus?: AppNavLinkStatus; +} & ({ + path: string; + deepLinks?: AppDeepLink[]; +} | { + path?: string; + deepLinks: AppDeepLink[]; +}); + // @public export type AppLeaveAction = AppLeaveDefaultAction | AppLeaveConfirmAction; @@ -142,12 +157,6 @@ export interface ApplicationStart { navigateToUrl(url: string): Promise; } -// @public -export interface AppMeta { - keywords?: string[]; - searchDeepLinks?: AppSearchDeepLink[]; -} - // @public export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; @@ -170,20 +179,6 @@ export enum AppNavLinkStatus { visible = 1 } -// @public -export type AppSearchDeepLink = { - id: string; - title: string; -} & ({ - path: string; - searchDeepLinks?: AppSearchDeepLink[]; - keywords?: string[]; -} | { - path?: string; - searchDeepLinks: AppSearchDeepLink[]; - keywords?: string[]; -}); - // @public export enum AppStatus { accessible = 0, @@ -194,7 +189,7 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: App) => Partial | undefined; @@ -1071,23 +1066,19 @@ export interface PluginInitializerContext export type PluginOpaqueId = symbol; // @public -export type PublicAppInfo = Omit & { - status: AppStatus; - navLinkStatus: AppNavLinkStatus; - appRoute: string; - meta: PublicAppMetaInfo; -}; - -// @public -export type PublicAppMetaInfo = Omit & { +export type PublicAppDeepLinkInfo = Omit & { + deepLinks: PublicAppDeepLinkInfo[]; keywords: string[]; - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; + navLinkStatus: AppNavLinkStatus; }; // @public -export type PublicAppSearchDeepLinkInfo = Omit & { - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; +export type PublicAppInfo = Omit & { + status: AppStatus; + navLinkStatus: AppNavLinkStatus; + appRoute: string; keywords: string[]; + deepLinks: PublicAppDeepLinkInfo[]; }; // @public diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 936b41e7682bb7..3a748f3ceb6fd3 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -49,6 +49,13 @@ top: $headerHeight; height: calc(100% - #{$headerHeight}); } + + @include euiBreakpoint('m', 'l', 'xl') { + .euiPageSideBar--sticky { + max-height: calc(100vh - #{$headerHeight}); + top: #{$headerHeight}; + } + } } .kbnBody { diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 3b2feeecabb7c0..2f54bd1d818b56 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -276,7 +276,7 @@ kibana_vars=( xpack.reporting.roles.allow xpack.reporting.roles.enabled xpack.rollup.enabled - xpack.ruleRegistry.unsafe.write.enabled + xpack.ruleRegistry.write.enabled xpack.searchprofiler.enabled xpack.security.audit.enabled xpack.security.audit.appender.type diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index e9f5d206de9180..5ccf614533164a 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -7,7 +7,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { Plugin, CoreSetup, AppMountParameters, AppSearchDeepLink } from 'src/core/public'; +import { Plugin, CoreSetup, AppMountParameters, AppDeepLink } from 'src/core/public'; import { AppUpdater } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { sortBy } from 'lodash'; @@ -86,7 +86,7 @@ export class DevToolsPlugin implements Plugin { this.appStateUpdater.next(() => ({ navLinkStatus: AppNavLinkStatus.hidden })); } else { this.appStateUpdater.next(() => { - const deepLinks: AppSearchDeepLink[] = [...this.devTools.values()] + const deepLinks: AppDeepLink[] = [...this.devTools.values()] .filter( // Some tools do not use a string title, so we filter those out (tool) => !tool.enableRouting && !tool.isDisabled() && typeof tool.title === 'string' @@ -96,7 +96,7 @@ export class DevToolsPlugin implements Plugin { title: tool.title as string, path: `#/${tool.id}`, })); - return { meta: { searchDeepLinks: deepLinks } }; + return { deepLinks }; }); } } diff --git a/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap b/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap index 89fa05615a0391..a80e3a67fb2db0 100644 --- a/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap @@ -2,6 +2,7 @@ exports[`KibanaPageTemplate render basic template 1`] = ` `; exports[`KibanaPageTemplate render custom empty prompt only 1`] = ` @@ -33,6 +45,7 @@ exports[`KibanaPageTemplate render custom empty prompt only 1`] = ` exports[`KibanaPageTemplate render custom empty prompt with page header 1`] = ` @@ -58,6 +76,12 @@ exports[`KibanaPageTemplate render custom empty prompt with page header 1`] = ` exports[`KibanaPageTemplate render default empty prompt 1`] = ` @@ -72,7 +96,76 @@ exports[`KibanaPageTemplate render default empty prompt 1`] = ` test

} + iconColor="" iconType="test" />
`; + +exports[`KibanaPageTemplate render solutionNav 1`] = ` + + } + pageSideBarProps={ + Object { + "className": "kbnPageTemplate__pageSideBar", + } + } + restrictWidth={true} +/> +`; diff --git a/src/plugins/kibana_react/public/page_template/page_template.scss b/src/plugins/kibana_react/public/page_template/page_template.scss new file mode 100644 index 00000000000000..4b8513311114d8 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/page_template.scss @@ -0,0 +1,15 @@ +$euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7); + +.kbnPageTemplate__pageSideBar { + padding: $euiSizeL; + background: + linear-gradient(160deg, $euiSideNavEmphasizedBackgroundColor 0, $euiSideNavEmphasizedBackgroundColor $euiSizeXL, rgba(#FFF, 0) 0), + linear-gradient(175deg, $euiSideNavEmphasizedBackgroundColor 0, $euiSideNavEmphasizedBackgroundColor $euiSize, rgba(#FFF, 0) 0); +} + +@include euiBreakpoint('xs','s') { + .kbnPageTemplate__pageSideBar { + width: auto; + padding: 0; + } +} diff --git a/src/plugins/kibana_react/public/page_template/page_template.test.tsx b/src/plugins/kibana_react/public/page_template/page_template.test.tsx index 2ad9a81e7916c9..43f96a6c2b98cf 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.test.tsx +++ b/src/plugins/kibana_react/public/page_template/page_template.test.tsx @@ -10,6 +10,46 @@ import React from 'react'; import { shallow } from 'enzyme'; import { KibanaPageTemplate } from './page_template'; import { EuiEmptyPrompt } from '@elastic/eui'; +import { KibanaPageTemplateSolutionNavProps } from './solution_nav'; + +const navItems: KibanaPageTemplateSolutionNavProps['items'] = [ + { + name: 'Ingest', + id: '1', + items: [ + { + name: 'Ingest Node Pipelines', + id: '1.1', + }, + { + name: 'Logstash Pipelines', + id: '1.2', + }, + { + name: 'Beats Central Management', + id: '1.3', + }, + ], + }, + { + name: 'Data', + id: '2', + items: [ + { + name: 'Index Management', + id: '2.1', + }, + { + name: 'Index Lifecycle Policies', + id: '2.2', + }, + { + name: 'Snapshot and Restore', + id: '2.3', + }, + ], + }, +]; describe('KibanaPageTemplate', () => { test('render default empty prompt', () => { @@ -66,4 +106,23 @@ describe('KibanaPageTemplate', () => { ); expect(component).toMatchSnapshot(); }); + + test('render solutionNav', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/plugins/kibana_react/public/page_template/page_template.tsx b/src/plugins/kibana_react/public/page_template/page_template.tsx index eb834d00402ef0..0bbf97ca6ddb57 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.tsx +++ b/src/plugins/kibana_react/public/page_template/page_template.tsx @@ -5,10 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import './page_template.scss'; -import { EuiEmptyPrompt, EuiPageTemplate, EuiPageTemplateProps } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; +import classNames from 'classnames'; + +import { EuiEmptyPrompt, EuiPageTemplate, EuiPageTemplateProps } from '@elastic/eui'; + +import { + KibanaPageTemplateSolutionNav, + KibanaPageTemplateSolutionNavProps, +} from './solution_nav/solution_nav'; +/** + * A thin wrapper around EuiPageTemplate with a few Kibana specific additions + */ export type KibanaPageTemplateProps = EuiPageTemplateProps & { /** * Changes the template type depending on other props provided. @@ -17,6 +28,10 @@ export type KibanaPageTemplateProps = EuiPageTemplateProps & { * With `pageHeader` and `children`: Uses `centeredContent` */ isEmptyState?: boolean; + /** + * Quick creation of EuiSideNav. Hooks up mobile instance too + */ + solutionNav?: KibanaPageTemplateSolutionNavProps; }; export const KibanaPageTemplate: FunctionComponent = ({ @@ -27,6 +42,8 @@ export const KibanaPageTemplate: FunctionComponent = ({ restrictWidth = true, bottomBar, bottomBarProps, + pageSideBar, + solutionNav, ...rest }) => { // Needed for differentiating between union types @@ -38,6 +55,13 @@ export const KibanaPageTemplate: FunctionComponent = ({ }; } + /** + * Create the solution nav component + */ + if (solutionNav) { + pageSideBar = ; + } + /** * An easy way to create the right content for empty pages */ @@ -48,6 +72,7 @@ export const KibanaPageTemplate: FunctionComponent = ({ children = ( {pageTitle} : undefined} body={description ?

{description}

: undefined} actions={rightSideItems} @@ -62,8 +87,14 @@ export const KibanaPageTemplate: FunctionComponent = ({ return ( diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap new file mode 100644 index 00000000000000..02673577095348 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap @@ -0,0 +1,238 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KibanaPageTemplateSolutionNav accepts EuiSideNavProps 1`] = ` +
+ +

+ + Solution + +

+
+ + + + } + toggleOpenOnMobile={[Function]} + /> +
+`; + +exports[`KibanaPageTemplateSolutionNav renders 1`] = ` +
+ +

+ + Solution + +

+
+ + + + } + toggleOpenOnMobile={[Function]} + /> +
+`; + +exports[`KibanaPageTemplateSolutionNav renders with icon 1`] = ` +
+ +

+ + + Solution + +

+
+ + + + + } + toggleOpenOnMobile={[Function]} + /> +
+`; diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap new file mode 100644 index 00000000000000..ede09c5652c310 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KibanaPageTemplateSolutionNavAvatar renders 1`] = ` + +`; diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/index.ts b/src/plugins/kibana_react/public/page_template/solution_nav/index.ts new file mode 100644 index 00000000000000..abbcde9a084869 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { KibanaPageTemplateSolutionNav, KibanaPageTemplateSolutionNavProps } from './solution_nav'; +export { + KibanaPageTemplateSolutionNavAvatar, + KibanaPageTemplateSolutionNavAvatarProps, +} from './solution_nav_avatar'; diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss new file mode 100644 index 00000000000000..bdb88b2ab7baae --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss @@ -0,0 +1,22 @@ +.kbnPageTemplateSolutionNav__title { + margin-bottom: $euiSizeL; +} + +@include euiBreakpoint('xs','s') { + .kbnPageTemplateSolutionNav { + // TODO: Fix in EUI + .euiSideNav__mobileToggle { + height: auto; + font-size: $euiFontSizeM; + + .euiButtonEmpty__text { + overflow: visible; + } + } + } + + // Rely on the `mobileToggle` of the EuiSideNav component to title the navigation list + .kbnPageTemplateSolutionNav__title { + display: none; + } +} diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx new file mode 100644 index 00000000000000..1ba6cc924cda1f --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { KibanaPageTemplateSolutionNav, KibanaPageTemplateSolutionNavProps } from './solution_nav'; + +const items: KibanaPageTemplateSolutionNavProps['items'] = [ + { + name: 'Ingest', + id: '1', + items: [ + { + name: 'Ingest Node Pipelines', + id: '1.1', + }, + { + name: 'Logstash Pipelines', + id: '1.2', + }, + { + name: 'Beats Central Management', + id: '1.3', + }, + ], + }, + { + name: 'Data', + id: '2', + items: [ + { + name: 'Index Management', + id: '2.1', + }, + { + name: 'Index Lifecycle Policies', + id: '2.2', + }, + { + name: 'Snapshot and Restore', + id: '2.3', + }, + ], + }, +]; + +describe('KibanaPageTemplateSolutionNav', () => { + test('renders', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('renders with icon', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + test('accepts EuiSideNavProps', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx new file mode 100644 index 00000000000000..4aa456f716dbda --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import './solution_nav.scss'; + +import React, { FunctionComponent, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiTitle, EuiSideNav, EuiSideNavProps, htmlIdGenerator } from '@elastic/eui'; + +import { + KibanaPageTemplateSolutionNavAvatar, + KibanaPageTemplateSolutionNavAvatarProps, +} from './solution_nav_avatar'; + +export type KibanaPageTemplateSolutionNavProps = EuiSideNavProps<{}> & { + /** + * Name of the solution, i.e. "Observability" + */ + name: KibanaPageTemplateSolutionNavAvatarProps['name']; + /** + * Solution logo, i.e. "logoObservability" + */ + icon?: KibanaPageTemplateSolutionNavAvatarProps['iconType']; +}; + +/** + * A wrapper around EuiSideNav but also creates the appropriate title with optional solution logo + */ +export const KibanaPageTemplateSolutionNav: FunctionComponent = ({ + name, + icon, + items, + ...rest +}) => { + const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); + const toggleOpenOnMobile = () => { + setisSideNavOpenOnMobile(!isSideNavOpenOnMobile); + }; + + /** + * Create the avatar. + */ + let solutionAvatar; + if (icon) { + solutionAvatar = ; + } + + /** + * Create the required title. + * a11y: Since the heading can't be nested inside `